diff --git a/composer.lock b/composer.lock index 739ac6e..ed1f0a6 100644 --- a/composer.lock +++ b/composer.lock @@ -53,16 +53,16 @@ }, { "name": "claviska/simpleimage", - "version": "3.6.5", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/claviska/SimpleImage.git", - "reference": "00f90662686696b9b7157dbb176183aabe89700f" + "reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/00f90662686696b9b7157dbb176183aabe89700f", - "reference": "00f90662686696b9b7157dbb176183aabe89700f", + "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/abd15ced313c7b8041d7d73d8d2398b4f2510cf1", + "reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1", "shasum": "" }, "require": { @@ -90,7 +90,7 @@ "description": "A PHP class that makes working with images as simple as possible.", "support": { "issues": "https://github.com/claviska/SimpleImage/issues", - "source": "https://github.com/claviska/SimpleImage/tree/3.6.5" + "source": "https://github.com/claviska/SimpleImage/tree/3.7.0" }, "funding": [ { @@ -98,7 +98,7 @@ "type": "github" } ], - "time": "2021-12-01T12:42:55+00:00" + "time": "2022-07-05T13:18:44+00:00" }, { "name": "filp/whoops", @@ -173,35 +173,53 @@ }, { "name": "getkirby/cms", - "version": "3.6.3", + "version": "3.7.5", "source": { "type": "git", "url": "https://github.com/getkirby/kirby.git", - "reference": "6b20fa11843f57cd9a1e611bc9e8e8a91b855156" + "reference": "021561f7444896fc9917eccb52768a6e715e9a74" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getkirby/kirby/zipball/6b20fa11843f57cd9a1e611bc9e8e8a91b855156", - "reference": "6b20fa11843f57cd9a1e611bc9e8e8a91b855156", + "url": "https://api.github.com/repos/getkirby/kirby/zipball/021561f7444896fc9917eccb52768a6e715e9a74", + "reference": "021561f7444896fc9917eccb52768a6e715e9a74", "shasum": "" }, "require": { - "claviska/simpleimage": "3.6.5", + "claviska/simpleimage": "3.7.0", "ext-ctype": "*", + "ext-curl": "*", + "ext-dom": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-libxml": "*", "ext-mbstring": "*", + "ext-openssl": "*", + "ext-simplexml": "*", "filp/whoops": "2.14.5", "getkirby/composer-installer": "^1.2.1", - "laminas/laminas-escaper": "2.9.0", + "laminas/laminas-escaper": "2.10.0", "michelf/php-smartypants": "1.8.1", "php": ">=7.4.0 <8.2.0", - "phpmailer/phpmailer": "6.5.4", - "psr/log": "1.1.4", - "symfony/polyfill-intl-idn": "1.24.0", - "symfony/polyfill-mbstring": "1.24.0" + "phpmailer/phpmailer": "6.6.4", + "symfony/polyfill-intl-idn": "1.26.0", + "symfony/polyfill-mbstring": "1.26.0" }, "replace": { "symfony/polyfill-php72": "*" }, + "suggest": { + "ext-PDO": "Support for using databases", + "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-intl": "Improved i18n number formatting", + "ext-memcached": "Support for the Memcached cache driver", + "ext-zip": "Support for ZIP archive file functions", + "ext-zlib": "Sanitization and validation for svgz files" + }, "type": "kirby-cms", "extra": { "unused": [ @@ -250,7 +268,7 @@ "type": "custom" } ], - "time": "2022-03-22T09:36:50+00:00" + "time": "2022-08-30T18:27:48+00:00" }, { "name": "getkirby/composer-installer", @@ -338,33 +356,33 @@ }, { "name": "laminas/laminas-escaper", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-escaper.git", - "reference": "891ad70986729e20ed2e86355fcf93c9dc238a5f" + "reference": "58af67282db37d24e584a837a94ee55b9c7552be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/891ad70986729e20ed2e86355fcf93c9dc238a5f", - "reference": "891ad70986729e20ed2e86355fcf93c9dc238a5f", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/58af67282db37d24e584a837a94ee55b9c7552be", + "reference": "58af67282db37d24e584a837a94ee55b9c7552be", "shasum": "" }, "require": { - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "^7.4 || ~8.0.0 || ~8.1.0" }, "conflict": { "zendframework/zend-escaper": "*" }, "require-dev": { + "infection/infection": "^0.26.6", "laminas/laminas-coding-standard": "~2.3.0", - "phpunit/phpunit": "^9.3", - "psalm/plugin-phpunit": "^0.12.2", - "vimeo/psalm": "^3.16" - }, - "suggest": { - "ext-iconv": "*", - "ext-mbstring": "*" + "maglnet/composer-require-checker": "^3.8.0", + "phpunit/phpunit": "^9.5.18", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.22.0" }, "type": "library", "autoload": { @@ -396,7 +414,7 @@ "type": "community_bridge" } ], - "time": "2021-09-02T17:10:53+00:00" + "time": "2022-03-08T20:15:36+00:00" }, { "name": "league/color-extractor", @@ -512,21 +530,24 @@ }, { "name": "mullema/k3-image-clip", - "version": "3.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/mullema/k3-image-clip.git", - "reference": "ac6a4a461ae8972557da24755005a3937a275b0c" + "reference": "c2e01f2ceb9eb5bc56895177359d398a3a2dbcf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mullema/k3-image-clip/zipball/ac6a4a461ae8972557da24755005a3937a275b0c", - "reference": "ac6a4a461ae8972557da24755005a3937a275b0c", + "url": "https://api.github.com/repos/mullema/k3-image-clip/zipball/c2e01f2ceb9eb5bc56895177359d398a3a2dbcf4", + "reference": "c2e01f2ceb9eb5bc56895177359d398a3a2dbcf4", "shasum": "" }, "require": { "getkirby/composer-installer": "^1.2" }, + "conflict": { + "getkirby/cms": "<3.6" + }, "type": "kirby-plugin", "extra": { "installer-name": "k3-image-clip" @@ -545,22 +566,22 @@ "description": "Visual image clip for Kirby 3", "support": { "issues": "https://github.com/mullema/k3-image-clip/issues", - "source": "https://github.com/mullema/k3-image-clip/tree/3.0.0" + "source": "https://github.com/mullema/k3-image-clip/tree/3.1.0" }, - "time": "2021-12-05T21:47:42+00:00" + "time": "2022-06-26T08:59:46+00:00" }, { "name": "phpmailer/phpmailer", - "version": "v6.5.4", + "version": "v6.6.4", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "c0d9f7dd3c2aa247ca44791e9209233829d82285" + "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/c0d9f7dd3c2aa247ca44791e9209233829d82285", - "reference": "c0d9f7dd3c2aa247ca44791e9209233829d82285", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/a94fdebaea6bd17f51be0c2373ab80d3d681269b", + "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b", "shasum": "" }, "require": { @@ -572,8 +593,8 @@ "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "doctrine/annotations": "^1.2", - "php-parallel-lint/php-console-highlighter": "^0.5.0", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.3.5", "roave/security-advisories": "dev-latest", "squizlabs/php_codesniffer": "^3.6.2", @@ -617,7 +638,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.5.4" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.4" }, "funding": [ { @@ -625,34 +646,34 @@ "type": "github" } ], - "time": "2022-02-17T08:19:04+00:00" + "time": "2022-08-22T09:22:00+00:00" }, { "name": "psr/log", - "version": "1.1.4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", - "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001", + "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=8.0.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.1.x-dev" + "dev-master": "3.x-dev" } }, "autoload": { "psr-4": { - "Psr\\Log\\": "Psr/Log/" + "Psr\\Log\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -673,9 +694,9 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/1.1.4" + "source": "https://github.com/php-fig/log/tree/3.0.0" }, - "time": "2021-05-03T11:20:27+00:00" + "time": "2021-07-14T16:46:02+00:00" }, { "name": "sylvainjule/matomo", @@ -717,16 +738,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "30885182c981ab175d4d034db0f6f469898070ab" + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", - "reference": "30885182c981ab175d4d034db0f6f469898070ab", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", "shasum": "" }, "require": { @@ -741,7 +762,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -779,7 +800,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" }, "funding": [ { @@ -795,20 +816,20 @@ "type": "tidelift" } ], - "time": "2021-10-20T20:35:02+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "749045c69efb97c70d25d7463abba812e91f3a44" + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/749045c69efb97c70d25d7463abba812e91f3a44", - "reference": "749045c69efb97c70d25d7463abba812e91f3a44", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", "shasum": "" }, "require": { @@ -822,7 +843,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -866,7 +887,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" }, "funding": [ { @@ -882,20 +903,20 @@ "type": "tidelift" } ], - "time": "2021-09-14T14:02:44+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -907,7 +928,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -950,7 +971,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" }, "funding": [ { @@ -966,20 +987,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { @@ -994,7 +1015,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1033,7 +1054,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -1049,20 +1070,20 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "twig/twig", - "version": "v3.3.8", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "972d8604a92b7054828b539f2febb0211dd5945c" + "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/972d8604a92b7054828b539f2febb0211dd5945c", - "reference": "972d8604a92b7054828b539f2febb0211dd5945c", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", + "reference": "e07cdd3d430cd7e453c31b36eb5ad6c0c5e43077", "shasum": "" }, "require": { @@ -1077,7 +1098,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1113,7 +1134,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.3.8" + "source": "https://github.com/twigphp/Twig/tree/v3.4.2" }, "funding": [ { @@ -1125,7 +1146,7 @@ "type": "tidelift" } ], - "time": "2022-02-04T06:59:48+00:00" + "time": "2022-08-12T06:47:24+00:00" } ], "packages-dev": [], diff --git a/kirby/.editorconfig b/kirby/.editorconfig index a0ebce7..76df047 100644 --- a/kirby/.editorconfig +++ b/kirby/.editorconfig @@ -6,10 +6,19 @@ root = true -[*.php] +[*] charset = utf-8 end_of_line = lf -insert_final_newline = true +indent_style = tab +indent_size = 2 trim_trailing_whitespace = true + +[*.php] +indent_size = 4 +insert_final_newline = true + +[*.yml] indent_style = space -indent_size = 4 \ No newline at end of file + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/kirby/.vscode/extensions.json b/kirby/.vscode/extensions.json deleted file mode 100644 index 7efca3f..0000000 --- a/kirby/.vscode/extensions.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "recommendations": [ - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode" - ] -} diff --git a/kirby/.vscode/settings.json b/kirby/.vscode/settings.json deleted file mode 100644 index 9bf4d12..0000000 --- a/kirby/.vscode/settings.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true -} diff --git a/kirby/README.md b/kirby/README.md index 825ea33..2075a4f 100644 --- a/kirby/README.md +++ b/kirby/README.md @@ -28,7 +28,7 @@ Please post all bug reports in our [issue tracker](https://github.com/getkirby/k If you have ideas for a feature or enhancement for Kirby, please use our [feedback platform](https://feedback.getkirby.com). **Translations, bug fixes, code contributions ...** -Read about how to contribute to the development in our [contributing guide](/.github/CONTRIBUTING.md). +Read about how to contribute to the development in our [contributing guide](/CONTRIBUTING.md). diff --git a/kirby/bootstrap.php b/kirby/bootstrap.php index 15121d2..b4c1fa3 100644 --- a/kirby/bootstrap.php +++ b/kirby/bootstrap.php @@ -5,31 +5,28 @@ * stop at older or too recent versions */ if ( - version_compare(PHP_VERSION, '7.4.0', '>=') === false || - version_compare(PHP_VERSION, '8.2.0', '<') === false + version_compare(PHP_VERSION, '7.4.0', '>=') === false || + version_compare(PHP_VERSION, '8.2.0', '<') === false ) { - die(include __DIR__ . '/views/php.php'); + die(include __DIR__ . '/views/php.php'); } if (is_file($autoloader = dirname(__DIR__) . '/vendor/autoload.php')) { - - /** - * Always prefer a site-wide Composer autoloader - * if it exists, it means that the user has probably - * installed additional packages - */ - include $autoloader; + /** + * Always prefer a site-wide Composer autoloader + * if it exists, it means that the user has probably + * installed additional packages + */ + include $autoloader; } elseif (is_file($autoloader = __DIR__ . '/vendor/autoload.php')) { - - /** - * Fall back to the local autoloader if that exists - */ - include $autoloader; + /** + * Fall back to the local autoloader if that exists + */ + include $autoloader; } else { - - /** - * If neither one exists, don't bother searching; - * it's a custom directory setup and the users need to - * load the autoloader themselves - */ + /** + * If neither one exists, don't bother searching; + * it's a custom directory setup and the users need to + * load the autoloader themselves + */ } diff --git a/kirby/cacert.pem b/kirby/cacert.pem index e91e25f..6b70ee0 100644 --- a/kirby/cacert.pem +++ b/kirby/cacert.pem @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Fri Mar 18 12:29:51 2022 GMT +## Certificate data from Mozilla as of: Tue Jul 19 03:12:06 2022 GMT ## ## This is a bundle of X.509 certificates of public Certificate Authorities ## (CA). These were automatically extracted from Mozilla's root certificates @@ -14,7 +14,7 @@ ## Just configure this file as the SSLCACertificateFile. ## ## Conversion done with mk-ca-bundle.pl version 1.29. -## SHA256: 187ef9dc231135324fe78830cf4462f1ecdeab3e6c9d5e38d623391e88dc5d3c +## SHA256: 9bf3799611fb58197f61d45e71ce3dc19f30e7dd73731915872ce5108a7bb066 ## @@ -993,30 +993,6 @@ tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 -----END CERTIFICATE----- -Hellenic Academic and Research Institutions RootCA 2011 -======================================================= ------BEGIN CERTIFICATE----- -MIIEMTCCAxmgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBlTELMAkGA1UEBhMCR1IxRDBCBgNVBAoT -O0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9y -aXR5MUAwPgYDVQQDEzdIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z -IFJvb3RDQSAyMDExMB4XDTExMTIwNjEzNDk1MloXDTMxMTIwMTEzNDk1MlowgZUxCzAJBgNVBAYT -AkdSMUQwQgYDVQQKEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25z -IENlcnQuIEF1dGhvcml0eTFAMD4GA1UEAxM3SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNo -IEluc3RpdHV0aW9ucyBSb290Q0EgMjAxMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB -AKlTAOMupvaO+mDYLZU++CwqVE7NuYRhlFhPjz2L5EPzdYmNUeTDN9KKiE15HrcS3UN4SoqS5tdI -1Q+kOilENbgH9mgdVc04UfCMJDGFr4PJfel3r+0ae50X+bOdOFAPplp5kYCvN66m0zH7tSYJnTxa -71HFK9+WXesyHgLacEnsbgzImjeN9/E2YEsmLIKe0HjzDQ9jpFEw4fkrJxIH2Oq9GGKYsFk3fb7u -8yBRQlqD75O6aRXxYp2fmTmCobd0LovUxQt7L/DICto9eQqakxylKHJzkUOap9FNhYS5qXSPFEDH -3N6sQWRstBmbAmNtJGSPRLIl6s5ddAxjMlyNh+UCAwEAAaOBiTCBhjAPBgNVHRMBAf8EBTADAQH/ -MAsGA1UdDwQEAwIBBjAdBgNVHQ4EFgQUppFC/RNhSiOeCKQp5dgTBCPuQSUwRwYDVR0eBEAwPqA8 -MAWCAy5ncjAFggMuZXUwBoIELmVkdTAGggQub3JnMAWBAy5ncjAFgQMuZXUwBoEELmVkdTAGgQQu -b3JnMA0GCSqGSIb3DQEBBQUAA4IBAQAf73lB4XtuP7KMhjdCSk4cNx6NZrokgclPEg8hwAOXhiVt -XdMiKahsog2p6z0GW5k6x8zDmjR/qw7IThzh+uTczQ2+vyT+bOdrwg3IBp5OjWEopmr95fZi6hg8 -TqBTnbI6nOulnJEWtk2C4AwFSKls9cz4y51JtPACpf1wA+2KIaWuE4ZJwzNzvoc7dIsXRSZMFpGD -/md9zU1jZ/rzAxKWeAaNsWftjj++n08C9bMJL/NMh98qy5V8AcysNnq/onN694/BtZqhFLKPM58N -7yLcZnuEvUUXBj08yrl3NI/K6s8/MT7jiOOASSXIl7WdmplNsDz4SgCbZN2fOUvRJ9e4 ------END CERTIFICATE----- - Actalis Authentication Root CA ============================== -----BEGIN CERTIFICATE----- @@ -3279,3 +3255,206 @@ PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh 4rsUecrNIdSUtUlD -----END CERTIFICATE----- + +Telia Root CA v2 +================ +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQxCzAJBgNVBAYT +AkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2 +MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQK +DBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ7 +6zBqAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9vVYiQJ3q +9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9lRdU2HhE8Qx3FZLgmEKn +pNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTODn3WhUidhOPFZPY5Q4L15POdslv5e2QJl +tI5c0BE0312/UqeBAMN/mUWZFdUXyApT7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW +5olWK8jjfN7j/4nlNW4o6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNr +RBH0pUPCTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6WT0E +BXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63RDolUK5X6wK0dmBR4 +M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZIpEYslOqodmJHixBTB0hXbOKSTbau +BcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGjYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7W +xy+G2CQ5MB0GA1UdDgQWBBRyrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi0f6X+J8wfBj5 +tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMMA8iZGok1GTzTyVR8qPAs5m4H +eW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBSSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+C +y748fdHif64W1lZYudogsYMVoe+KTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygC +QMez2P2ccGrGKMOF6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15 +h2Er3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMtTy3EHD70 +sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pTVmBds9hCG1xLEooc6+t9 +xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAWysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQ +raVplI/owd8k+BsHMYeB2F326CjYSlKArBPuUBQemMc= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7 +dPYSzuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0QVK5buXu +QqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/VbNafAkl1bK6CKBrqx9t +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2JyX3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFWwKrY7RjEsK70Pvom +AjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHVdWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +D-TRUST EV Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8 +ZRCC/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rDwpdhQntJ +raOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3OqQo5FD4pPfsazK2/umL +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2V2X3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CAy/m0sRtW9XLS/BnR +AjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJbgfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +DigiCert TLS ECC P384 Root G5 +============================= +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURpZ2lDZXJ0IFRMUyBFQ0MgUDM4 +NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQg +Um9vdCBHNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1Tzvd +lHJS7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp0zVozptj +n4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICISB4CIfBFqMA4GA1UdDwEB +/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCJao1H5+z8blUD2Wds +Jk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIx +AJSdYsiJvRmEFOml+wG4DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +DigiCert TLS RSA4096 Root G5 +============================ +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBNMQswCQYDVQQG +EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0 +MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcNNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2 +IFJvb3QgRzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS8 +7IE+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG02C+JFvuU +AT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgpwgscONyfMXdcvyej/Ces +tyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZMpG2T6T867jp8nVid9E6P/DsjyG244gXa +zOvswzH016cpVIDPRFtMbzCe88zdH5RDnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnV +DdXifBBiqmvwPXbzP6PosMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9q +TXeXAaDxZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cdLvvy +z6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvXKyY//SovcfXWJL5/ +MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNeXoVPzthwiHvOAbWWl9fNff2C+MIk +wcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPLtgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4E +FgQUUTMc7TZArxfTJc1paPKvTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7HPNtQOa27PShN +lnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLFO4uJ+DQtpBflF+aZfTCIITfN +MBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/ +u4cnYiWB39yhL/btp/96j1EuMPikAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9G +OUrYU9DzLjtxpdRv/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh +47a+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilwMUc/dNAU +FvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WFqUITVuwhd4GTWgzqltlJ +yqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCKovfepEWFJqgejF0pW8hL2JpqA15w8oVP +bEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +Certainly Root R1 +================= +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAwPTELMAkGA1UE +BhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2VydGFpbmx5IFJvb3QgUjEwHhcN +MjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2Vy +dGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBANA21B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O +5MQTvqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbedaFySpvXl +8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b01C7jcvk2xusVtyWMOvwl +DbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGI +XsXwClTNSaa/ApzSRKft43jvRl5tcdF5cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkN +KPl6I7ENPT2a/Z2B7yyQwHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQ +AjeZjOVJ6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA2Cnb +rlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyHWyf5QBGenDPBt+U1 +VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMReiFPCyEQtkA6qyI6BJyLm4SGcprS +p6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTgqj8ljZ9EXME66C6ud0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAsz +HQNTVfSVcOQrPbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi1wrykXprOQ4v +MMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrdrRT90+7iIgXr0PK3aBLXWopB +GsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9ditaY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+ +gjwN/KUD+nsa2UUeYNrEjvn8K8l7lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgH +JBu6haEaBQmAupVjyTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7 +fpYnKx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLyyCwzk5Iw +x06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5nwXARPbv0+Em34yaXOp/S +X3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +Certainly Root E1 +================= +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQswCQYDVQQGEwJV +UzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBFMTAeFw0yMTA0 +MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlu +bHkxGjAYBgNVBAMTEUNlcnRhaW5seSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4 +fxzf7flHh4axpMCK+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9 +YBk2QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4hevIIgcwCgYIKoZIzj0E +AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 +rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +E-Tugra Global Root CA RSA v3 +============================= +-----BEGIN CERTIFICATE----- +MIIF8zCCA9ugAwIBAgIUDU3FzRYilZYIfrgLfxUGNPt5EDQwDQYJKoZIhvcNAQELBQAwgYAxCzAJ +BgNVBAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVncmEgRUJHIEEuUy4xHTAb +BgNVBAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290 +IENBIFJTQSB2MzAeFw0yMDAzMTgwOTA3MTdaFw00NTAzMTIwOTA3MTdaMIGAMQswCQYDVQQGEwJU +UjEPMA0GA1UEBxMGQW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRF +LVR1Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBSU0Eg +djMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCiZvCJt3J77gnJY9LTQ91ew6aEOErx +jYG7FL1H6EAX8z3DeEVypi6Q3po61CBxyryfHUuXCscxuj7X/iWpKo429NEvx7epXTPcMHD4QGxL +sqYxYdE0PD0xesevxKenhOGXpOhL9hd87jwH7eKKV9y2+/hDJVDqJ4GohryPUkqWOmAalrv9c/SF +/YP9f4RtNGx/ardLAQO/rWm31zLZ9Vdq6YaCPqVmMbMWPcLzJmAy01IesGykNz709a/r4d+ABs8q +QedmCeFLl+d3vSFtKbZnwy1+7dZ5ZdHPOrbRsV5WYVB6Ws5OUDGAA5hH5+QYfERaxqSzO8bGwzrw +bMOLyKSRBfP12baqBqG3q+Sx6iEUXIOk/P+2UNOMEiaZdnDpwA+mdPy70Bt4znKS4iicvObpCdg6 +04nmvi533wEKb5b25Y08TVJ2Glbhc34XrD2tbKNSEhhw5oBOM/J+JjKsBY04pOZ2PJ8QaQ5tndLB +eSBrW88zjdGUdjXnXVXHt6woq0bM5zshtQoK5EpZ3IE1S0SVEgpnpaH/WwAH0sDM+T/8nzPyAPiM +bIedBi3x7+PmBvrFZhNb/FAHnnGGstpvdDDPk1Po3CLW3iAfYY2jLqN4MpBs3KwytQXk9TwzDdbg +h3cXTJ2w2AmoDVf3RIXwyAS+XF1a4xeOVGNpf0l0ZAWMowIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MB8GA1UdIwQYMBaAFLK0ruYt9ybVqnUtdkvAG1Mh0EjvMB0GA1UdDgQWBBSytK7mLfcm1ap1 +LXZLwBtTIdBI7zAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAImocn+M684uGMQQ +gC0QDP/7FM0E4BQ8Tpr7nym/Ip5XuYJzEmMmtcyQ6dIqKe6cLcwsmb5FJ+Sxce3kOJUxQfJ9emN4 +38o2Fi+CiJ+8EUdPdk3ILY7r3y18Tjvarvbj2l0Upq7ohUSdBm6O++96SmotKygY/r+QLHUWnw/q +ln0F7psTpURs+APQ3SPh/QMSEgj0GDSz4DcLdxEBSL9htLX4GdnLTeqjjO/98Aa1bZL0SmFQhO3s +SdPkvmjmLuMxC1QLGpLWgti2omU8ZgT5Vdps+9u1FGZNlIM7zR6mK7L+d0CGq+ffCsn99t2HVhjY +sCxVYJb6CH5SkPVLpi6HfMsg2wY+oF0Dd32iPBMbKaITVaA9FCKvb7jQmhty3QUBjYZgv6Rn7rWl +DdF/5horYmbDB7rnoEgcOMPpRfunf/ztAmgayncSd6YAVSgU7NbHEqIbZULpkejLPoeJVF3Zr52X +nGnnCv8PWniLYypMfUeUP95L6VPQMPHF9p5J3zugkaOj/s1YzOrfr28oO6Bpm4/srK4rVJ2bBLFH +IK+WEj5jlB0E5y67hscMmoi/dkfv97ALl2bSRM9gUgfh1SxKOidhd8rXj+eHDjD/DLsE4mHDosiX +YY60MGo8bcIHX0pzLz/5FooBZu+6kcpSV3uu1OYP3Qt6f4ueJiDPO++BcYNZ +-----END CERTIFICATE----- + +E-Tugra Global Root CA ECC v3 +============================= +-----BEGIN CERTIFICATE----- +MIICpTCCAiqgAwIBAgIUJkYZdzHhT28oNt45UYbm1JeIIsEwCgYIKoZIzj0EAwMwgYAxCzAJBgNV +BAYTAlRSMQ8wDQYDVQQHEwZBbmthcmExGTAXBgNVBAoTEEUtVHVncmEgRUJHIEEuUy4xHTAbBgNV +BAsTFEUtVHVncmEgVHJ1c3QgQ2VudGVyMSYwJAYDVQQDEx1FLVR1Z3JhIEdsb2JhbCBSb290IENB +IEVDQyB2MzAeFw0yMDAzMTgwOTQ2NThaFw00NTAzMTIwOTQ2NThaMIGAMQswCQYDVQQGEwJUUjEP +MA0GA1UEBxMGQW5rYXJhMRkwFwYDVQQKExBFLVR1Z3JhIEVCRyBBLlMuMR0wGwYDVQQLExRFLVR1 +Z3JhIFRydXN0IENlbnRlcjEmMCQGA1UEAxMdRS1UdWdyYSBHbG9iYWwgUm9vdCBDQSBFQ0MgdjMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAASOmCm/xxAeJ9urA8woLNheSBkQKczLWYHMjLiSF4mDKpL2 +w6QdTGLVn9agRtwcvHbB40fQWxPa56WzZkjnIZpKT4YKfWzqTTKACrJ6CZtpS5iB4i7sAnCWH/31 +Rs7K3IKjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU/4Ixcj75xGZsrTie0bBRiKWQ +zPUwHQYDVR0OBBYEFP+CMXI++cRmbK04ntGwUYilkMz1MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNpADBmAjEA5gVYaWHlLcoNy/EZCL3W/VGSGn5jVASQkZo1kTmZ+gepZpO6yGjUij/67W4W +Aie3AjEA3VoXK3YdZUKWpqxdinlW2Iob35reX8dQj7FbcQwm32pAAOwzkSFxvmjkI6TZraE3 +-----END CERTIFICATE----- diff --git a/kirby/composer.json b/kirby/composer.json index ff3beb4..94059bb 100644 --- a/kirby/composer.json +++ b/kirby/composer.json @@ -1,90 +1,110 @@ { - "name": "getkirby/cms", - "description": "The Kirby 3 core", - "license": "proprietary", - "type": "kirby-cms", - "version": "3.6.3", - "keywords": [ - "kirby", - "cms", - "core" - ], - "authors": [ - { - "name": "Kirby Team", - "email": "support@getkirby.com", - "homepage": "https://getkirby.com" - } - ], - "homepage": "https://getkirby.com", - "support": { - "email": "support@getkirby.com", - "issues": "https://github.com/getkirby/kirby/issues", - "forum": "https://forum.getkirby.com", - "source": "https://github.com/getkirby/kirby" - }, - "_comment": "TODO: psr/log is not used by Kirby; drop pinned version when Kirby no longer supports PHP 7", - "require": { - "php": ">=7.4.0 <8.2.0", - "ext-ctype": "*", - "ext-mbstring": "*", - "claviska/simpleimage": "3.6.5", - "filp/whoops": "2.14.5", - "getkirby/composer-installer": "^1.2.1", - "laminas/laminas-escaper": "2.9.0", - "michelf/php-smartypants": "1.8.1", - "phpmailer/phpmailer": "6.5.4", - "psr/log": "1.1.4", - "symfony/polyfill-intl-idn": "1.24.0", - "symfony/polyfill-mbstring": "1.24.0" - }, - "replace": { - "symfony/polyfill-php72": "*" - }, - "autoload": { - "psr-4": { - "Kirby\\": "src/" - }, - "classmap": [ - "dependencies/" - ], - "files": [ - "config/setup.php", - "config/helpers.php" - ] - }, - "config": { - "allow-plugins": { - "getkirby/composer-installer": true - }, - "optimize-autoloader": true, - "platform-check": false - }, - "extra": { - "unused": [ - "symfony/polyfill-intl-idn" - ] - }, - "scripts": { - "post-update-cmd": "curl -o cacert.pem https://curl.se/ca/cacert.pem", - "analyze": [ - "@analyze:composer", - "@analyze:psalm", - "@analyze:phpcpd", - "@analyze:phpmd" - ], - "analyze:composer": "composer validate --strict --no-check-version --no-check-all", - "analyze:phpcpd": "phpcpd --fuzzy --exclude tests --exclude vendor .", - "analyze:phpmd": "phpmd . ansi phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*'", - "analyze:psalm": "psalm", - "build": "./scripts/build", - "ci": [ - "@fix", - "@analyze", - "@test" - ], - "fix": "php-cs-fixer fix", - "test": "phpunit --stderr --coverage-html=tests/coverage", - "zip": "composer archive --format=zip --file=dist" - } + "name": "getkirby/cms", + "description": "The Kirby 3 core", + "license": "proprietary", + "type": "kirby-cms", + "version": "3.7.5", + "keywords": [ + "kirby", + "cms", + "core" + ], + "authors": [ + { + "name": "Kirby Team", + "email": "support@getkirby.com", + "homepage": "https://getkirby.com" + } + ], + "homepage": "https://getkirby.com", + "support": { + "email": "support@getkirby.com", + "issues": "https://github.com/getkirby/kirby/issues", + "forum": "https://forum.getkirby.com", + "source": "https://github.com/getkirby/kirby" + }, + "require": { + "php": ">=7.4.0 <8.2.0", + "ext-SimpleXML": "*", + "ext-ctype": "*", + "ext-curl": "*", + "ext-dom": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "claviska/simpleimage": "3.7.0", + "filp/whoops": "2.14.5", + "getkirby/composer-installer": "^1.2.1", + "laminas/laminas-escaper": "2.10.0", + "michelf/php-smartypants": "1.8.1", + "phpmailer/phpmailer": "6.6.4", + "symfony/polyfill-intl-idn": "1.26.0", + "symfony/polyfill-mbstring": "1.26.0" + }, + "replace": { + "symfony/polyfill-php72": "*" + }, + "suggest": { + "ext-PDO": "Support for using databases", + "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-intl": "Improved i18n number formatting", + "ext-memcached": "Support for the Memcached cache driver", + "ext-zip": "Support for ZIP archive file functions", + "ext-zlib": "Sanitization and validation for svgz files" + }, + "autoload": { + "psr-4": { + "Kirby\\": "src/" + }, + "classmap": [ + "dependencies/" + ], + "files": [ + "config/setup.php", + "config/helpers.php" + ] + }, + "config": { + "allow-plugins": { + "getkirby/composer-installer": true + }, + "optimize-autoloader": true, + "platform": { + "php": "7.4.0" + }, + "platform-check": false + }, + "extra": { + "unused": [ + "symfony/polyfill-intl-idn" + ] + }, + "scripts": { + "post-update-cmd": "curl -o cacert.pem https://curl.se/ca/cacert.pem", + "analyze": [ + "@analyze:composer", + "@analyze:psalm", + "@analyze:phpcpd", + "@analyze:phpmd" + ], + "analyze:composer": "composer validate --strict --no-check-version --no-check-all", + "analyze:phpcpd": "phpcpd --fuzzy --exclude tests --exclude vendor .", + "analyze:phpmd": "phpmd . ansi phpmd.xml.dist --exclude 'dependencies/*,tests/*,vendor/*'", + "analyze:psalm": "psalm", + "build": "./scripts/build", + "ci": [ + "@fix", + "@analyze", + "@test" + ], + "fix": "php-cs-fixer fix", + "test": "phpunit --stderr --coverage-html=tests/coverage", + "zip": "composer archive --format=zip --file=dist" + } } diff --git a/kirby/composer.lock b/kirby/composer.lock index 2b33619..eb7aeaf 100644 --- a/kirby/composer.lock +++ b/kirby/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d4cf75084dae428fe0ab54124637d51f", + "content-hash": "fb087946fb5ac5910e25a4d263905d99", "packages": [ { "name": "claviska/simpleimage", - "version": "3.6.5", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/claviska/SimpleImage.git", - "reference": "00f90662686696b9b7157dbb176183aabe89700f" + "reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/00f90662686696b9b7157dbb176183aabe89700f", - "reference": "00f90662686696b9b7157dbb176183aabe89700f", + "url": "https://api.github.com/repos/claviska/SimpleImage/zipball/abd15ced313c7b8041d7d73d8d2398b4f2510cf1", + "reference": "abd15ced313c7b8041d7d73d8d2398b4f2510cf1", "shasum": "" }, "require": { @@ -45,7 +45,7 @@ "description": "A PHP class that makes working with images as simple as possible.", "support": { "issues": "https://github.com/claviska/SimpleImage/issues", - "source": "https://github.com/claviska/SimpleImage/tree/3.6.5" + "source": "https://github.com/claviska/SimpleImage/tree/3.7.0" }, "funding": [ { @@ -53,7 +53,7 @@ "type": "github" } ], - "time": "2021-12-01T12:42:55+00:00" + "time": "2022-07-05T13:18:44+00:00" }, { "name": "filp/whoops", @@ -175,33 +175,33 @@ }, { "name": "laminas/laminas-escaper", - "version": "2.9.0", + "version": "2.10.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-escaper.git", - "reference": "891ad70986729e20ed2e86355fcf93c9dc238a5f" + "reference": "58af67282db37d24e584a837a94ee55b9c7552be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/891ad70986729e20ed2e86355fcf93c9dc238a5f", - "reference": "891ad70986729e20ed2e86355fcf93c9dc238a5f", + "url": "https://api.github.com/repos/laminas/laminas-escaper/zipball/58af67282db37d24e584a837a94ee55b9c7552be", + "reference": "58af67282db37d24e584a837a94ee55b9c7552be", "shasum": "" }, "require": { - "php": "^7.3 || ~8.0.0 || ~8.1.0" + "ext-ctype": "*", + "ext-mbstring": "*", + "php": "^7.4 || ~8.0.0 || ~8.1.0" }, "conflict": { "zendframework/zend-escaper": "*" }, "require-dev": { + "infection/infection": "^0.26.6", "laminas/laminas-coding-standard": "~2.3.0", - "phpunit/phpunit": "^9.3", - "psalm/plugin-phpunit": "^0.12.2", - "vimeo/psalm": "^3.16" - }, - "suggest": { - "ext-iconv": "*", - "ext-mbstring": "*" + "maglnet/composer-require-checker": "^3.8.0", + "phpunit/phpunit": "^9.5.18", + "psalm/plugin-phpunit": "^0.16.1", + "vimeo/psalm": "^4.22.0" }, "type": "library", "autoload": { @@ -233,7 +233,7 @@ "type": "community_bridge" } ], - "time": "2021-09-02T17:10:53+00:00" + "time": "2022-03-08T20:15:36+00:00" }, { "name": "league/color-extractor", @@ -349,16 +349,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.5.4", + "version": "v6.6.4", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "c0d9f7dd3c2aa247ca44791e9209233829d82285" + "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/c0d9f7dd3c2aa247ca44791e9209233829d82285", - "reference": "c0d9f7dd3c2aa247ca44791e9209233829d82285", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/a94fdebaea6bd17f51be0c2373ab80d3d681269b", + "reference": "a94fdebaea6bd17f51be0c2373ab80d3d681269b", "shasum": "" }, "require": { @@ -370,8 +370,8 @@ "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", "doctrine/annotations": "^1.2", - "php-parallel-lint/php-console-highlighter": "^0.5.0", - "php-parallel-lint/php-parallel-lint": "^1.3.1", + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.3.5", "roave/security-advisories": "dev-latest", "squizlabs/php_codesniffer": "^3.6.2", @@ -415,7 +415,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.5.4" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.6.4" }, "funding": [ { @@ -423,7 +423,7 @@ "type": "github" } ], - "time": "2022-02-17T08:19:04+00:00" + "time": "2022-08-22T09:22:00+00:00" }, { "name": "psr/log", @@ -477,16 +477,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "749045c69efb97c70d25d7463abba812e91f3a44" + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/749045c69efb97c70d25d7463abba812e91f3a44", - "reference": "749045c69efb97c70d25d7463abba812e91f3a44", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", "shasum": "" }, "require": { @@ -500,7 +500,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -544,7 +544,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" }, "funding": [ { @@ -560,20 +560,20 @@ "type": "tidelift" } ], - "time": "2021-09-14T14:02:44+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.25.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", - "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", "shasum": "" }, "require": { @@ -585,7 +585,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -628,7 +628,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" }, "funding": [ { @@ -644,20 +644,20 @@ "type": "tidelift" } ], - "time": "2021-02-19T12:13:01+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.24.0", + "version": "v1.26.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", - "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", + "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e", "shasum": "" }, "require": { @@ -672,7 +672,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.23-dev" + "dev-main": "1.26-dev" }, "thanks": { "name": "symfony/polyfill", @@ -711,7 +711,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.24.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0" }, "funding": [ { @@ -727,7 +727,7 @@ "type": "tidelift" } ], - "time": "2021-11-30T18:21:41+00:00" + "time": "2022-05-24T11:49:31+00:00" } ], "packages-dev": [], @@ -738,9 +738,21 @@ "prefer-lowest": false, "platform": { "php": ">=7.4.0 <8.2.0", + "ext-simplexml": "*", "ext-ctype": "*", - "ext-mbstring": "*" + "ext-curl": "*", + "ext-dom": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-iconv": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-openssl": "*" }, "platform-dev": [], - "plugin-api-version": "2.1.0" + "platform-overrides": { + "php": "7.4.0" + }, + "plugin-api-version": "2.3.0" } diff --git a/kirby/config/aliases.php b/kirby/config/aliases.php index 7366e6c..ee795a3 100644 --- a/kirby/config/aliases.php +++ b/kirby/config/aliases.php @@ -1,80 +1,81 @@ 'Kirby\Cms\Collection', - 'field' => 'Kirby\Cms\Field', - 'file' => 'Kirby\Cms\File', - 'files' => 'Kirby\Cms\Files', - 'find' => 'Kirby\Cms\Find', - 'html' => 'Kirby\Cms\Html', - 'kirby' => 'Kirby\Cms\App', - 'page' => 'Kirby\Cms\Page', - 'pages' => 'Kirby\Cms\Pages', - 'pagination' => 'Kirby\Cms\Pagination', - 'r' => 'Kirby\Cms\R', - 'response' => 'Kirby\Cms\Response', - 's' => 'Kirby\Cms\S', - 'sane' => 'Kirby\Sane\Sane', - 'site' => 'Kirby\Cms\Site', - 'structure' => 'Kirby\Cms\Structure', - 'url' => 'Kirby\Cms\Url', - 'user' => 'Kirby\Cms\User', - 'users' => 'Kirby\Cms\Users', - 'visitor' => 'Kirby\Cms\Visitor', + // cms classes + 'collection' => 'Kirby\Cms\Collection', + 'field' => 'Kirby\Cms\Field', + 'file' => 'Kirby\Cms\File', + 'files' => 'Kirby\Cms\Files', + 'find' => 'Kirby\Cms\Find', + 'helpers' => 'Kirby\Cms\Helpers', + 'html' => 'Kirby\Cms\Html', + 'kirby' => 'Kirby\Cms\App', + 'page' => 'Kirby\Cms\Page', + 'pages' => 'Kirby\Cms\Pages', + 'pagination' => 'Kirby\Cms\Pagination', + 'r' => 'Kirby\Cms\R', + 'response' => 'Kirby\Cms\Response', + 's' => 'Kirby\Cms\S', + 'sane' => 'Kirby\Sane\Sane', + 'site' => 'Kirby\Cms\Site', + 'structure' => 'Kirby\Cms\Structure', + 'url' => 'Kirby\Cms\Url', + 'user' => 'Kirby\Cms\User', + 'users' => 'Kirby\Cms\Users', + 'visitor' => 'Kirby\Cms\Visitor', - // data handler - 'data' => 'Kirby\Data\Data', - 'json' => 'Kirby\Data\Json', - 'yaml' => 'Kirby\Data\Yaml', + // data handler + 'data' => 'Kirby\Data\Data', + 'json' => 'Kirby\Data\Json', + 'yaml' => 'Kirby\Data\Yaml', - // file classes - 'asset' => 'Kirby\Filesystem\Asset', - 'dir' => 'Kirby\Filesystem\Dir', - 'f' => 'Kirby\Filesystem\F', - 'mime' => 'Kirby\Filesystem\Mime', + // file classes + 'asset' => 'Kirby\Filesystem\Asset', + 'dir' => 'Kirby\Filesystem\Dir', + 'f' => 'Kirby\Filesystem\F', + 'mime' => 'Kirby\Filesystem\Mime', - // data classes - 'database' => 'Kirby\Database\Database', - 'db' => 'Kirby\Database\Db', + // data classes + 'database' => 'Kirby\Database\Database', + 'db' => 'Kirby\Database\Db', - // exceptions - 'errorpageexception' => 'Kirby\Exception\ErrorPageException', + // exceptions + 'errorpageexception' => 'Kirby\Exception\ErrorPageException', - // http classes - 'cookie' => 'Kirby\Http\Cookie', - 'header' => 'Kirby\Http\Header', - 'remote' => 'Kirby\Http\Remote', - 'server' => 'Kirby\Http\Server', + // http classes + 'cookie' => 'Kirby\Http\Cookie', + 'header' => 'Kirby\Http\Header', + 'remote' => 'Kirby\Http\Remote', + 'server' => 'Kirby\Http\Server', - // image classes - 'dimensions' => 'Kirby\Image\Dimensions', + // image classes + 'dimensions' => 'Kirby\Image\Dimensions', - // panel classes - 'panel' => 'Kirby\Panel\Panel', + // panel classes + 'panel' => 'Kirby\Panel\Panel', - // toolkit classes - 'a' => 'Kirby\Toolkit\A', - 'c' => 'Kirby\Toolkit\Config', - 'config' => 'Kirby\Toolkit\Config', - 'escape' => 'Kirby\Toolkit\Escape', - 'i18n' => 'Kirby\Toolkit\I18n', - 'obj' => 'Kirby\Toolkit\Obj', - 'str' => 'Kirby\Toolkit\Str', - 'tpl' => 'Kirby\Toolkit\Tpl', - 'v' => 'Kirby\Toolkit\V', - 'xml' => 'Kirby\Toolkit\Xml', + // toolkit classes + 'a' => 'Kirby\Toolkit\A', + 'c' => 'Kirby\Toolkit\Config', + 'config' => 'Kirby\Toolkit\Config', + 'escape' => 'Kirby\Toolkit\Escape', + 'i18n' => 'Kirby\Toolkit\I18n', + 'obj' => 'Kirby\Toolkit\Obj', + 'str' => 'Kirby\Toolkit\Str', + 'tpl' => 'Kirby\Toolkit\Tpl', + 'v' => 'Kirby\Toolkit\V', + 'xml' => 'Kirby\Toolkit\Xml', - // TODO: remove in 4.0.0 - 'kirby\cms\asset' => 'Kirby\Filesystem\Asset', - 'kirby\cms\dir' => 'Kirby\Filesystem\Dir', - 'kirby\cms\filename' => 'Kirby\Filesystem\Filename', - 'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile', - 'kirby\cms\form' => 'Kirby\Form\Form', - 'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag', - 'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags', - 'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir', - 'kirby\toolkit\f' => 'Kirby\Filesystem\F', - 'kirby\toolkit\file' => 'Kirby\Filesystem\File', - 'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime', + // TODO: remove in 4.0.0 + 'kirby\cms\asset' => 'Kirby\Filesystem\Asset', + 'kirby\cms\dir' => 'Kirby\Filesystem\Dir', + 'kirby\cms\filename' => 'Kirby\Filesystem\Filename', + 'kirby\cms\filefoundation' => 'Kirby\Filesystem\IsFile', + 'kirby\cms\form' => 'Kirby\Form\Form', + 'kirby\cms\kirbytag' => 'Kirby\Text\KirbyTag', + 'kirby\cms\kirbytags' => 'Kirby\Text\KirbyTags', + 'kirby\toolkit\dir' => 'Kirby\Filesystem\Dir', + 'kirby\toolkit\f' => 'Kirby\Filesystem\F', + 'kirby\toolkit\file' => 'Kirby\Filesystem\File', + 'kirby\toolkit\mime' => 'Kirby\Filesystem\Mime', ]; diff --git a/kirby/config/api/authentication.php b/kirby/config/api/authentication.php index b089940..4758599 100644 --- a/kirby/config/api/authentication.php +++ b/kirby/config/api/authentication.php @@ -3,25 +3,25 @@ use Kirby\Exception\PermissionException; return function () { - $auth = $this->kirby()->auth(); - $allowImpersonation = $this->kirby()->option('api.allowImpersonation') ?? false; + $auth = $this->kirby()->auth(); + $allowImpersonation = $this->kirby()->option('api.allowImpersonation') ?? false; - // csrf token check - if ( - $auth->type($allowImpersonation) === 'session' && - $auth->csrf() === false - ) { - throw new PermissionException('Unauthenticated'); - } + // csrf token check + if ( + $auth->type($allowImpersonation) === 'session' && + $auth->csrf() === false + ) { + throw new PermissionException('Unauthenticated'); + } - // get user from session or basic auth - if ($user = $auth->user(null, $allowImpersonation)) { - if ($user->role()->permissions()->for('access', 'panel') === false) { - throw new PermissionException(['key' => 'access.panel']); - } + // get user from session or basic auth + if ($user = $auth->user(null, $allowImpersonation)) { + if ($user->role()->permissions()->for('access', 'panel') === false) { + throw new PermissionException(['key' => 'access.panel']); + } - return $user; - } + return $user; + } - throw new PermissionException('Unauthenticated'); + throw new PermissionException('Unauthenticated'); }; diff --git a/kirby/config/api/collections.php b/kirby/config/api/collections.php index fb3e218..3a34927 100644 --- a/kirby/config/api/collections.php +++ b/kirby/config/api/collections.php @@ -5,66 +5,66 @@ */ return [ - /** - * Children - */ - 'children' => [ - 'model' => 'page', - 'type' => 'Kirby\Cms\Pages', - 'view' => 'compact' - ], + /** + * Children + */ + 'children' => [ + 'model' => 'page', + 'type' => 'Kirby\Cms\Pages', + 'view' => 'compact' + ], - /** - * Files - */ - 'files' => [ - 'model' => 'file', - 'type' => 'Kirby\Cms\Files' - ], + /** + * Files + */ + 'files' => [ + 'model' => 'file', + 'type' => 'Kirby\Cms\Files' + ], - /** - * Languages - */ - 'languages' => [ - 'model' => 'language', - 'type' => 'Kirby\Cms\Languages' - ], + /** + * Languages + */ + 'languages' => [ + 'model' => 'language', + 'type' => 'Kirby\Cms\Languages' + ], - /** - * Pages - */ - 'pages' => [ - 'model' => 'page', - 'type' => 'Kirby\Cms\Pages', - 'view' => 'compact' - ], + /** + * Pages + */ + 'pages' => [ + 'model' => 'page', + 'type' => 'Kirby\Cms\Pages', + 'view' => 'compact' + ], - /** - * Roles - */ - 'roles' => [ - 'model' => 'role', - 'type' => 'Kirby\Cms\Roles', - 'view' => 'compact' - ], + /** + * Roles + */ + 'roles' => [ + 'model' => 'role', + 'type' => 'Kirby\Cms\Roles', + 'view' => 'compact' + ], - /** - * Translations - */ - 'translations' => [ - 'model' => 'translation', - 'type' => 'Kirby\Cms\Translations', - 'view' => 'compact' - ], + /** + * Translations + */ + 'translations' => [ + 'model' => 'translation', + 'type' => 'Kirby\Cms\Translations', + 'view' => 'compact' + ], - /** - * Users - */ - 'users' => [ - 'default' => fn () => $this->users(), - 'model' => 'user', - 'type' => 'Kirby\Cms\Users', - 'view' => 'compact' - ] + /** + * Users + */ + 'users' => [ + 'default' => fn () => $this->users(), + 'model' => 'user', + 'type' => 'Kirby\Cms\Users', + 'view' => 'compact' + ] ]; diff --git a/kirby/config/api/models.php b/kirby/config/api/models.php index 51d19fb..075442f 100644 --- a/kirby/config/api/models.php +++ b/kirby/config/api/models.php @@ -4,17 +4,17 @@ * Api Model Definitions */ return [ - 'File' => include __DIR__ . '/models/File.php', - 'FileBlueprint' => include __DIR__ . '/models/FileBlueprint.php', - 'FileVersion' => include __DIR__ . '/models/FileVersion.php', - 'Language' => include __DIR__ . '/models/Language.php', - 'Page' => include __DIR__ . '/models/Page.php', - 'PageBlueprint' => include __DIR__ . '/models/PageBlueprint.php', - 'Role' => include __DIR__ . '/models/Role.php', - 'Site' => include __DIR__ . '/models/Site.php', - 'SiteBlueprint' => include __DIR__ . '/models/SiteBlueprint.php', - 'System' => include __DIR__ . '/models/System.php', - 'Translation' => include __DIR__ . '/models/Translation.php', - 'User' => include __DIR__ . '/models/User.php', - 'UserBlueprint' => include __DIR__ . '/models/UserBlueprint.php', + 'File' => include __DIR__ . '/models/File.php', + 'FileBlueprint' => include __DIR__ . '/models/FileBlueprint.php', + 'FileVersion' => include __DIR__ . '/models/FileVersion.php', + 'Language' => include __DIR__ . '/models/Language.php', + 'Page' => include __DIR__ . '/models/Page.php', + 'PageBlueprint' => include __DIR__ . '/models/PageBlueprint.php', + 'Role' => include __DIR__ . '/models/Role.php', + 'Site' => include __DIR__ . '/models/Site.php', + 'SiteBlueprint' => include __DIR__ . '/models/SiteBlueprint.php', + 'System' => include __DIR__ . '/models/System.php', + 'Translation' => include __DIR__ . '/models/Translation.php', + 'User' => include __DIR__ . '/models/User.php', + 'UserBlueprint' => include __DIR__ . '/models/UserBlueprint.php', ]; diff --git a/kirby/config/api/models/File.php b/kirby/config/api/models/File.php index 64fde1d..dac185d 100644 --- a/kirby/config/api/models/File.php +++ b/kirby/config/api/models/File.php @@ -7,116 +7,109 @@ use Kirby\Form\Form; * File */ return [ - 'fields' => [ - 'blueprint' => fn (File $file) => $file->blueprint(), - 'content' => fn (File $file) => Form::for($file)->values(), - 'dimensions' => fn (File $file) => $file->dimensions()->toArray(), - 'dragText' => fn (File $file) => $file->panel()->dragText(), - 'exists' => fn (File $file) => $file->exists(), - 'extension' => fn (File $file) => $file->extension(), - 'filename' => fn (File $file) => $file->filename(), - 'id' => fn (File $file) => $file->id(), - 'link' => fn (File $file) => $file->panel()->url(true), - 'mime' => fn (File $file) => $file->mime(), - 'modified' => fn (File $file) => $file->modified('c'), - 'name' => fn (File $file) => $file->name(), - 'next' => fn (File $file) => $file->next(), - 'nextWithTemplate' => function (File $file) { - $files = $file->templateSiblings()->sorted(); - $index = $files->indexOf($file); + 'fields' => [ + 'blueprint' => fn (File $file) => $file->blueprint(), + 'content' => fn (File $file) => Form::for($file)->values(), + 'dimensions' => fn (File $file) => $file->dimensions()->toArray(), + 'dragText' => fn (File $file) => $file->panel()->dragText(), + 'exists' => fn (File $file) => $file->exists(), + 'extension' => fn (File $file) => $file->extension(), + 'filename' => fn (File $file) => $file->filename(), + 'id' => fn (File $file) => $file->id(), + 'link' => fn (File $file) => $file->panel()->url(true), + 'mime' => fn (File $file) => $file->mime(), + 'modified' => fn (File $file) => $file->modified('c'), + 'name' => fn (File $file) => $file->name(), + 'next' => fn (File $file) => $file->next(), + 'nextWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sorted(); + $index = $files->indexOf($file); - return $files->nth($index + 1); - }, - 'niceSize' => fn (File $file) => $file->niceSize(), - 'options' => fn (File $file) => $file->panel()->options(), - 'panelIcon' => function (File $file) { - // TODO: remove in 3.7.0 - // @codeCoverageIgnoreStart - deprecated('The API field file.panelIcon has been deprecated and will be removed in 3.7.0. Use file.panelImage instead'); - return $file->panel()->image(); - // @codeCoverageIgnoreEnd - }, - 'panelImage' => fn (File $file) => $file->panel()->image(), - 'panelUrl' => fn (File $file) => $file->panel()->url(true), - 'prev' => fn (File $file) => $file->prev(), - 'prevWithTemplate' => function (File $file) { - $files = $file->templateSiblings()->sorted(); - $index = $files->indexOf($file); + return $files->nth($index + 1); + }, + 'niceSize' => fn (File $file) => $file->niceSize(), + 'options' => fn (File $file) => $file->panel()->options(), + 'panelImage' => fn (File $file) => $file->panel()->image(), + 'panelUrl' => fn (File $file) => $file->panel()->url(true), + 'prev' => fn (File $file) => $file->prev(), + 'prevWithTemplate' => function (File $file) { + $files = $file->templateSiblings()->sorted(); + $index = $files->indexOf($file); - return $files->nth($index - 1); - }, - 'parent' => fn (File $file) => $file->parent(), - 'parents' => fn (File $file) => $file->parents()->flip(), - 'size' => fn (File $file) => $file->size(), - 'template' => fn (File $file) => $file->template(), - 'thumbs' => function ($file) { - if ($file->isResizable() === false) { - return null; - } + return $files->nth($index - 1); + }, + 'parent' => fn (File $file) => $file->parent(), + 'parents' => fn (File $file) => $file->parents()->flip(), + 'size' => fn (File $file) => $file->size(), + 'template' => fn (File $file) => $file->template(), + 'thumbs' => function ($file) { + if ($file->isResizable() === false) { + return null; + } - return [ - 'tiny' => $file->resize(128)->url(), - 'small' => $file->resize(256)->url(), - 'medium' => $file->resize(512)->url(), - 'large' => $file->resize(768)->url(), - 'huge' => $file->resize(1024)->url(), - ]; - }, - 'type' => fn (File $file) => $file->type(), - 'url' => fn (File $file) => $file->url(), - ], - 'type' => 'Kirby\Cms\File', - 'views' => [ - 'default' => [ - 'content', - 'dimensions', - 'exists', - 'extension', - 'filename', - 'id', - 'link', - 'mime', - 'modified', - 'name', - 'next' => 'compact', - 'niceSize', - 'parent' => 'compact', - 'options', - 'prev' => 'compact', - 'size', - 'template', - 'type', - 'url' - ], - 'compact' => [ - 'filename', - 'id', - 'link', - 'type', - 'url', - ], - 'panel' => [ - 'blueprint', - 'content', - 'dimensions', - 'extension', - 'filename', - 'id', - 'link', - 'mime', - 'modified', - 'name', - 'nextWithTemplate' => 'compact', - 'niceSize', - 'options', - 'panelIcon', - 'panelImage', - 'parent' => 'compact', - 'parents' => ['id', 'slug', 'title'], - 'prevWithTemplate' => 'compact', - 'template', - 'type', - 'url' - ] - ], + return [ + 'tiny' => $file->resize(128)->url(), + 'small' => $file->resize(256)->url(), + 'medium' => $file->resize(512)->url(), + 'large' => $file->resize(768)->url(), + 'huge' => $file->resize(1024)->url(), + ]; + }, + 'type' => fn (File $file) => $file->type(), + 'url' => fn (File $file) => $file->url(), + ], + 'type' => 'Kirby\Cms\File', + 'views' => [ + 'default' => [ + 'content', + 'dimensions', + 'exists', + 'extension', + 'filename', + 'id', + 'link', + 'mime', + 'modified', + 'name', + 'next' => 'compact', + 'niceSize', + 'parent' => 'compact', + 'options', + 'prev' => 'compact', + 'size', + 'template', + 'type', + 'url' + ], + 'compact' => [ + 'filename', + 'id', + 'link', + 'type', + 'url', + ], + 'panel' => [ + 'blueprint', + 'content', + 'dimensions', + 'extension', + 'filename', + 'id', + 'link', + 'mime', + 'modified', + 'name', + 'nextWithTemplate' => 'compact', + 'niceSize', + 'options', + 'panelIcon', + 'panelImage', + 'parent' => 'compact', + 'parents' => ['id', 'slug', 'title'], + 'prevWithTemplate' => 'compact', + 'template', + 'type', + 'url' + ] + ], ]; diff --git a/kirby/config/api/models/FileBlueprint.php b/kirby/config/api/models/FileBlueprint.php index 5279f8a..e781e45 100644 --- a/kirby/config/api/models/FileBlueprint.php +++ b/kirby/config/api/models/FileBlueprint.php @@ -6,13 +6,13 @@ use Kirby\Cms\FileBlueprint; * FileBlueprint */ return [ - 'fields' => [ - 'name' => fn (FileBlueprint $blueprint) => $blueprint->name(), - 'options' => fn (FileBlueprint $blueprint) => $blueprint->options(), - 'tabs' => fn (FileBlueprint $blueprint) => $blueprint->tabs(), - 'title' => fn (FileBlueprint $blueprint) => $blueprint->title(), - ], - 'type' => 'Kirby\Cms\FileBlueprint', - 'views' => [ - ], + 'fields' => [ + 'name' => fn (FileBlueprint $blueprint) => $blueprint->name(), + 'options' => fn (FileBlueprint $blueprint) => $blueprint->options(), + 'tabs' => fn (FileBlueprint $blueprint) => $blueprint->tabs(), + 'title' => fn (FileBlueprint $blueprint) => $blueprint->title(), + ], + 'type' => 'Kirby\Cms\FileBlueprint', + 'views' => [ + ], ]; diff --git a/kirby/config/api/models/FileVersion.php b/kirby/config/api/models/FileVersion.php index d5cea11..df2aac0 100644 --- a/kirby/config/api/models/FileVersion.php +++ b/kirby/config/api/models/FileVersion.php @@ -6,54 +6,54 @@ use Kirby\Cms\FileVersion; * FileVersion */ return [ - 'fields' => [ - 'dimensions' => fn (FileVersion $file) => $file->dimensions()->toArray(), - 'exists' => fn (FileVersion $file) => $file->exists(), - 'extension' => fn (FileVersion $file) => $file->extension(), - 'filename' => fn (FileVersion $file) => $file->filename(), - 'id' => fn (FileVersion $file) => $file->id(), - 'mime' => fn (FileVersion $file) => $file->mime(), - 'modified' => fn (FileVersion $file) => $file->modified('c'), - 'name' => fn (FileVersion $file) => $file->name(), - 'niceSize' => fn (FileVersion $file) => $file->niceSize(), - 'size' => fn (FileVersion $file) => $file->size(), - 'type' => fn (FileVersion $file) => $file->type(), - 'url' => fn (FileVersion $file) => $file->url(), - ], - 'type' => 'Kirby\Cms\FileVersion', - 'views' => [ - 'default' => [ - 'dimensions', - 'exists', - 'extension', - 'filename', - 'id', - 'mime', - 'modified', - 'name', - 'niceSize', - 'size', - 'type', - 'url' - ], - 'compact' => [ - 'filename', - 'id', - 'type', - 'url', - ], - 'panel' => [ - 'dimensions', - 'extension', - 'filename', - 'id', - 'mime', - 'modified', - 'name', - 'niceSize', - 'template', - 'type', - 'url' - ] - ], + 'fields' => [ + 'dimensions' => fn (FileVersion $file) => $file->dimensions()->toArray(), + 'exists' => fn (FileVersion $file) => $file->exists(), + 'extension' => fn (FileVersion $file) => $file->extension(), + 'filename' => fn (FileVersion $file) => $file->filename(), + 'id' => fn (FileVersion $file) => $file->id(), + 'mime' => fn (FileVersion $file) => $file->mime(), + 'modified' => fn (FileVersion $file) => $file->modified('c'), + 'name' => fn (FileVersion $file) => $file->name(), + 'niceSize' => fn (FileVersion $file) => $file->niceSize(), + 'size' => fn (FileVersion $file) => $file->size(), + 'type' => fn (FileVersion $file) => $file->type(), + 'url' => fn (FileVersion $file) => $file->url(), + ], + 'type' => 'Kirby\Cms\FileVersion', + 'views' => [ + 'default' => [ + 'dimensions', + 'exists', + 'extension', + 'filename', + 'id', + 'mime', + 'modified', + 'name', + 'niceSize', + 'size', + 'type', + 'url' + ], + 'compact' => [ + 'filename', + 'id', + 'type', + 'url', + ], + 'panel' => [ + 'dimensions', + 'extension', + 'filename', + 'id', + 'mime', + 'modified', + 'name', + 'niceSize', + 'template', + 'type', + 'url' + ] + ], ]; diff --git a/kirby/config/api/models/Language.php b/kirby/config/api/models/Language.php index 1e76e14..362d6f5 100644 --- a/kirby/config/api/models/Language.php +++ b/kirby/config/api/models/Language.php @@ -6,25 +6,25 @@ use Kirby\Cms\Language; * Language */ return [ - 'fields' => [ - 'code' => fn (Language $language) => $language->code(), - 'default' => fn (Language $language) => $language->isDefault(), - 'direction' => fn (Language $language) => $language->direction(), - 'locale' => fn (Language $language) => $language->locale(), - 'name' => fn (Language $language) => $language->name(), - 'rules' => fn (Language $language) => $language->rules(), - 'url' => fn (Language $language) => $language->url(), - ], - 'type' => 'Kirby\Cms\Language', - 'views' => [ - 'default' => [ - 'code', - 'default', - 'direction', - 'locale', - 'name', - 'rules', - 'url' - ] - ] + 'fields' => [ + 'code' => fn (Language $language) => $language->code(), + 'default' => fn (Language $language) => $language->isDefault(), + 'direction' => fn (Language $language) => $language->direction(), + 'locale' => fn (Language $language) => $language->locale(), + 'name' => fn (Language $language) => $language->name(), + 'rules' => fn (Language $language) => $language->rules(), + 'url' => fn (Language $language) => $language->url(), + ], + 'type' => 'Kirby\Cms\Language', + 'views' => [ + 'default' => [ + 'code', + 'default', + 'direction', + 'locale', + 'name', + 'rules', + 'url' + ] + ] ]; diff --git a/kirby/config/api/models/Page.php b/kirby/config/api/models/Page.php index d188e3d..03b6c7d 100644 --- a/kirby/config/api/models/Page.php +++ b/kirby/config/api/models/Page.php @@ -1,5 +1,6 @@ [ - 'blueprint' => fn (Page $page) => $page->blueprint(), - 'blueprints' => fn (Page $page) => $page->blueprints(), - 'children' => fn (Page $page) => $page->children(), - 'content' => fn (Page $page) => Form::for($page)->values(), - 'drafts' => fn (Page $page) => $page->drafts(), - 'errors' => fn (Page $page) => $page->errors(), - 'files' => fn (Page $page) => $page->files()->sorted(), - 'hasChildren' => fn (Page $page) => $page->hasChildren(), - 'hasDrafts' => fn (Page $page) => $page->hasDrafts(), - 'hasFiles' => fn (Page $page) => $page->hasFiles(), - 'id' => fn (Page $page) => $page->id(), - 'isSortable' => fn (Page $page) => $page->isSortable(), - /** - * @deprecated 3.6.0 - * @todo Throw deprecated warning in 3.7.0 - * @todo Remove in 3.8.0 - * @codeCoverageIgnore - */ - 'next' => function (Page $page) { - return $page - ->nextAll() - ->filter('intendedTemplate', $page->intendedTemplate()) - ->filter('status', $page->status()) - ->filter('isReadable', true) - ->first(); - }, - 'num' => fn (Page $page) => $page->num(), - 'options' => fn (Page $page) => $page->panel()->options(['preview']), - /** - * @todo Remove in 3.7.0 - * @codeCoverageIgnore - */ - 'panelIcon' => function (Page $page) { - deprecated('The API field page.panelIcon has been deprecated and will be removed in 3.7.0. Use page.panelImage instead'); - return $page->panel()->image(); - }, - 'panelImage' => fn (Page $page) => $page->panel()->image(), - 'parent' => fn (Page $page) => $page->parent(), - 'parents' => fn (Page $page) => $page->parents()->flip(), - /** - * @deprecated 3.6.0 - * @todo Throw deprecated warning in 3.7.0 - * @todo Remove in 3.8.0 - * @codeCoverageIgnore - */ - 'prev' => function (Page $page) { - return $page - ->prevAll() - ->filter('intendedTemplate', $page->intendedTemplate()) - ->filter('status', $page->status()) - ->filter('isReadable', true) - ->last(); - }, - 'previewUrl' => fn (Page $page) => $page->previewUrl(), - 'siblings' => function (Page $page) { - if ($page->isDraft() === true) { - return $page->parentModel()->children()->not($page); - } else { - return $page->siblings(); - } - }, - 'slug' => fn (Page $page) => $page->slug(), - 'status' => fn (Page $page) => $page->status(), - 'template' => fn (Page $page) => $page->intendedTemplate()->name(), - 'title' => fn (Page $page) => $page->title()->value(), - 'url' => fn (Page $page) => $page->url(), - ], - 'type' => 'Kirby\Cms\Page', - 'views' => [ - 'compact' => [ - 'id', - 'title', - 'url', - 'num' - ], - 'default' => [ - 'content', - 'id', - 'status', - 'num', - 'options', - 'parent' => 'compact', - 'slug', - 'template', - 'title', - 'url' - ], - 'panel' => [ - 'id', - 'blueprint', - 'content', - 'status', - 'options', - 'next' => ['id', 'slug', 'title'], - 'parents' => ['id', 'slug', 'title'], - 'prev' => ['id', 'slug', 'title'], - 'previewUrl', - 'slug', - 'title', - 'url' - ], - 'selector' => [ - 'id', - 'title', - 'parent' => [ - 'id', - 'title' - ], - 'children' => [ - 'hasChildren', - 'id', - 'panelIcon', - 'panelImage', - 'title', - ], - ] - ], + 'fields' => [ + 'blueprint' => fn (Page $page) => $page->blueprint(), + 'blueprints' => fn (Page $page) => $page->blueprints(), + 'children' => fn (Page $page) => $page->children(), + 'content' => fn (Page $page) => Form::for($page)->values(), + 'drafts' => fn (Page $page) => $page->drafts(), + 'errors' => fn (Page $page) => $page->errors(), + 'files' => fn (Page $page) => $page->files()->sorted(), + 'hasChildren' => fn (Page $page) => $page->hasChildren(), + 'hasDrafts' => fn (Page $page) => $page->hasDrafts(), + 'hasFiles' => fn (Page $page) => $page->hasFiles(), + 'id' => fn (Page $page) => $page->id(), + 'isSortable' => fn (Page $page) => $page->isSortable(), + /** + * @deprecated 3.6.0 + * @todo Remove in 3.8.0 + * @codeCoverageIgnore + */ + 'next' => function (Page $page) { + Helpers::deprecated('The API field page.next has been deprecated and will be removed in 3.8.0.'); + + return $page + ->nextAll() + ->filter('intendedTemplate', $page->intendedTemplate()) + ->filter('status', $page->status()) + ->filter('isReadable', true) + ->first(); + }, + 'num' => fn (Page $page) => $page->num(), + 'options' => fn (Page $page) => $page->panel()->options(['preview']), + 'panelImage' => fn (Page $page) => $page->panel()->image(), + 'parent' => fn (Page $page) => $page->parent(), + 'parents' => fn (Page $page) => $page->parents()->flip(), + /** + * @deprecated 3.6.0 + * @todo Remove in 3.8.0 + * @codeCoverageIgnore + */ + 'prev' => function (Page $page) { + Helpers::deprecated('The API field page.prev has been deprecated and will be removed in 3.8.0.'); + + return $page + ->prevAll() + ->filter('intendedTemplate', $page->intendedTemplate()) + ->filter('status', $page->status()) + ->filter('isReadable', true) + ->last(); + }, + 'previewUrl' => fn (Page $page) => $page->previewUrl(), + 'siblings' => function (Page $page) { + if ($page->isDraft() === true) { + return $page->parentModel()->children()->not($page); + } else { + return $page->siblings(); + } + }, + 'slug' => fn (Page $page) => $page->slug(), + 'status' => fn (Page $page) => $page->status(), + 'template' => fn (Page $page) => $page->intendedTemplate()->name(), + 'title' => fn (Page $page) => $page->title()->value(), + 'url' => fn (Page $page) => $page->url(), + ], + 'type' => 'Kirby\Cms\Page', + 'views' => [ + 'compact' => [ + 'id', + 'title', + 'url', + 'num' + ], + 'default' => [ + 'content', + 'id', + 'status', + 'num', + 'options', + 'parent' => 'compact', + 'slug', + 'template', + 'title', + 'url' + ], + 'panel' => [ + 'id', + 'blueprint', + 'content', + 'status', + 'options', + 'next' => ['id', 'slug', 'title'], + 'parents' => ['id', 'slug', 'title'], + 'prev' => ['id', 'slug', 'title'], + 'previewUrl', + 'slug', + 'title', + 'url' + ], + 'selector' => [ + 'id', + 'title', + 'parent' => [ + 'id', + 'title' + ], + 'children' => [ + 'hasChildren', + 'id', + 'panelIcon', + 'panelImage', + 'title', + ], + ] + ], ]; diff --git a/kirby/config/api/models/PageBlueprint.php b/kirby/config/api/models/PageBlueprint.php index c5de408..dc240ed 100644 --- a/kirby/config/api/models/PageBlueprint.php +++ b/kirby/config/api/models/PageBlueprint.php @@ -6,16 +6,16 @@ use Kirby\Cms\PageBlueprint; * PageBlueprint */ return [ - 'fields' => [ - 'name' => fn (PageBlueprint $blueprint) => $blueprint->name(), - 'num' => fn (PageBlueprint $blueprint) => $blueprint->num(), - 'options' => fn (PageBlueprint $blueprint) => $blueprint->options(), - 'preview' => fn (PageBlueprint $blueprint) => $blueprint->preview(), - 'status' => fn (PageBlueprint $blueprint) => $blueprint->status(), - 'tabs' => fn (PageBlueprint $blueprint) => $blueprint->tabs(), - 'title' => fn (PageBlueprint $blueprint) => $blueprint->title(), - ], - 'type' => 'Kirby\Cms\PageBlueprint', - 'views' => [ - ], + 'fields' => [ + 'name' => fn (PageBlueprint $blueprint) => $blueprint->name(), + 'num' => fn (PageBlueprint $blueprint) => $blueprint->num(), + 'options' => fn (PageBlueprint $blueprint) => $blueprint->options(), + 'preview' => fn (PageBlueprint $blueprint) => $blueprint->preview(), + 'status' => fn (PageBlueprint $blueprint) => $blueprint->status(), + 'tabs' => fn (PageBlueprint $blueprint) => $blueprint->tabs(), + 'title' => fn (PageBlueprint $blueprint) => $blueprint->title(), + ], + 'type' => 'Kirby\Cms\PageBlueprint', + 'views' => [ + ], ]; diff --git a/kirby/config/api/models/Role.php b/kirby/config/api/models/Role.php index 93a9e01..e1d27c3 100644 --- a/kirby/config/api/models/Role.php +++ b/kirby/config/api/models/Role.php @@ -6,18 +6,18 @@ use Kirby\Cms\Role; * Role */ return [ - 'fields' => [ - 'description' => fn (Role $role) => $role->description(), - 'name' => fn (Role $role) => $role->name(), - 'permissions' => fn (Role $role) => $role->permissions()->toArray(), - 'title' => fn (Role $role) => $role->title(), - ], - 'type' => 'Kirby\Cms\Role', - 'views' => [ - 'compact' => [ - 'description', - 'name', - 'title' - ] - ] + 'fields' => [ + 'description' => fn (Role $role) => $role->description(), + 'name' => fn (Role $role) => $role->name(), + 'permissions' => fn (Role $role) => $role->permissions()->toArray(), + 'title' => fn (Role $role) => $role->title(), + ], + 'type' => 'Kirby\Cms\Role', + 'views' => [ + 'compact' => [ + 'description', + 'name', + 'title' + ] + ] ]; diff --git a/kirby/config/api/models/Site.php b/kirby/config/api/models/Site.php index 4f5463c..c312ef4 100644 --- a/kirby/config/api/models/Site.php +++ b/kirby/config/api/models/Site.php @@ -7,46 +7,46 @@ use Kirby\Form\Form; * Site */ return [ - 'default' => fn () => $this->site(), - 'fields' => [ - 'blueprint' => fn (Site $site) => $site->blueprint(), - 'children' => fn (Site $site) => $site->children(), - 'content' => fn (Site $site) => Form::for($site)->values(), - 'drafts' => fn (Site $site) => $site->drafts(), - 'files' => fn (Site $site) => $site->files()->sorted(), - 'options' => fn (Site $site) => $site->permissions()->toArray(), - 'previewUrl' => fn (Site $site) => $site->previewUrl(), - 'title' => fn (Site $site) => $site->title()->value(), - 'url' => fn (Site $site) => $site->url(), - ], - 'type' => 'Kirby\Cms\Site', - 'views' => [ - 'compact' => [ - 'title', - 'url' - ], - 'default' => [ - 'content', - 'options', - 'title', - 'url' - ], - 'panel' => [ - 'title', - 'blueprint', - 'content', - 'options', - 'previewUrl', - 'url' - ], - 'selector' => [ - 'title', - 'children' => [ - 'id', - 'title', - 'panelIcon', - 'hasChildren' - ], - ] - ] + 'default' => fn () => $this->site(), + 'fields' => [ + 'blueprint' => fn (Site $site) => $site->blueprint(), + 'children' => fn (Site $site) => $site->children(), + 'content' => fn (Site $site) => Form::for($site)->values(), + 'drafts' => fn (Site $site) => $site->drafts(), + 'files' => fn (Site $site) => $site->files()->sorted(), + 'options' => fn (Site $site) => $site->permissions()->toArray(), + 'previewUrl' => fn (Site $site) => $site->previewUrl(), + 'title' => fn (Site $site) => $site->title()->value(), + 'url' => fn (Site $site) => $site->url(), + ], + 'type' => 'Kirby\Cms\Site', + 'views' => [ + 'compact' => [ + 'title', + 'url' + ], + 'default' => [ + 'content', + 'options', + 'title', + 'url' + ], + 'panel' => [ + 'title', + 'blueprint', + 'content', + 'options', + 'previewUrl', + 'url' + ], + 'selector' => [ + 'title', + 'children' => [ + 'id', + 'title', + 'panelIcon', + 'hasChildren' + ], + ] + ] ]; diff --git a/kirby/config/api/models/SiteBlueprint.php b/kirby/config/api/models/SiteBlueprint.php index c940212..7841841 100644 --- a/kirby/config/api/models/SiteBlueprint.php +++ b/kirby/config/api/models/SiteBlueprint.php @@ -6,12 +6,12 @@ use Kirby\Cms\SiteBlueprint; * SiteBlueprint */ return [ - 'fields' => [ - 'name' => fn (SiteBlueprint $blueprint) => $blueprint->name(), - 'options' => fn (SiteBlueprint $blueprint) => $blueprint->options(), - 'tabs' => fn (SiteBlueprint $blueprint) => $blueprint->tabs(), - 'title' => fn (SiteBlueprint $blueprint) => $blueprint->title(), - ], - 'type' => 'Kirby\Cms\SiteBlueprint', - 'views' => [], + 'fields' => [ + 'name' => fn (SiteBlueprint $blueprint) => $blueprint->name(), + 'options' => fn (SiteBlueprint $blueprint) => $blueprint->options(), + 'tabs' => fn (SiteBlueprint $blueprint) => $blueprint->tabs(), + 'title' => fn (SiteBlueprint $blueprint) => $blueprint->title(), + ], + 'type' => 'Kirby\Cms\SiteBlueprint', + 'views' => [], ]; diff --git a/kirby/config/api/models/System.php b/kirby/config/api/models/System.php index 0ad10eb..260aa45 100644 --- a/kirby/config/api/models/System.php +++ b/kirby/config/api/models/System.php @@ -7,92 +7,92 @@ use Kirby\Toolkit\Str; * System */ return [ - 'fields' => [ - 'ascii' => fn () => Str::$ascii, - 'authStatus' => fn () => $this->kirby()->auth()->status()->toArray(), - 'defaultLanguage' => fn () => $this->kirby()->panelLanguage(), - 'isOk' => fn (System $system) => $system->isOk(), - 'isInstallable' => fn (System $system) => $system->isInstallable(), - 'isInstalled' => fn (System $system) => $system->isInstalled(), - 'isLocal' => fn (System $system) => $system->isLocal(), - 'multilang' => fn () => $this->kirby()->option('languages', false) !== false, - 'languages' => fn () => $this->kirby()->languages(), - 'license' => fn (System $system) => $system->license(), - 'locales' => function () { - $locales = []; - $translations = $this->kirby()->translations(); - foreach ($translations as $translation) { - $locales[$translation->code()] = $translation->locale(); - } - return $locales; - }, - 'loginMethods' => fn (System $system) => array_keys($system->loginMethods()), - 'requirements' => fn (System $system) => $system->toArray(), - 'site' => fn (System $system) => $system->title(), - 'slugs' => fn () => Str::$language, - 'title' => fn () => $this->site()->title()->value(), - 'translation' => function () { - if ($user = $this->user()) { - $translationCode = $user->language(); - } else { - $translationCode = $this->kirby()->panelLanguage(); - } + 'fields' => [ + 'ascii' => fn () => Str::$ascii, + 'authStatus' => fn () => $this->kirby()->auth()->status()->toArray(), + 'defaultLanguage' => fn () => $this->kirby()->panelLanguage(), + 'isOk' => fn (System $system) => $system->isOk(), + 'isInstallable' => fn (System $system) => $system->isInstallable(), + 'isInstalled' => fn (System $system) => $system->isInstalled(), + 'isLocal' => fn (System $system) => $system->isLocal(), + 'multilang' => fn () => $this->kirby()->option('languages', false) !== false, + 'languages' => fn () => $this->kirby()->languages(), + 'license' => fn (System $system) => $system->license(), + 'locales' => function () { + $locales = []; + $translations = $this->kirby()->translations(); + foreach ($translations as $translation) { + $locales[$translation->code()] = $translation->locale(); + } + return $locales; + }, + 'loginMethods' => fn (System $system) => array_keys($system->loginMethods()), + 'requirements' => fn (System $system) => $system->toArray(), + 'site' => fn (System $system) => $system->title(), + 'slugs' => fn () => Str::$language, + 'title' => fn () => $this->site()->title()->value(), + 'translation' => function () { + if ($user = $this->user()) { + $translationCode = $user->language(); + } else { + $translationCode = $this->kirby()->panelLanguage(); + } - if ($translation = $this->kirby()->translation($translationCode)) { - return $translation; - } else { - return $this->kirby()->translation('en'); - } - }, - 'kirbytext' => fn () => $this->kirby()->option('panel.kirbytext') ?? true, - 'user' => fn () => $this->user(), - 'version' => function () { - $user = $this->user(); + if ($translation = $this->kirby()->translation($translationCode)) { + return $translation; + } else { + return $this->kirby()->translation('en'); + } + }, + 'kirbytext' => fn () => $this->kirby()->option('panel.kirbytext') ?? true, + 'user' => fn () => $this->user(), + 'version' => function () { + $user = $this->user(); - if ($user && $user->role()->permissions()->for('access', 'system') === true) { - return $this->kirby()->version(); - } else { - return null; - } - } - ], - 'type' => 'Kirby\Cms\System', - 'views' => [ - 'login' => [ - 'authStatus', - 'isOk', - 'isInstallable', - 'isInstalled', - 'loginMethods', - 'title', - 'translation' - ], - 'troubleshooting' => [ - 'isOk', - 'isInstallable', - 'isInstalled', - 'title', - 'translation', - 'requirements' - ], - 'panel' => [ - 'ascii', - 'defaultLanguage', - 'isOk', - 'isInstalled', - 'isLocal', - 'kirbytext', - 'languages', - 'license', - 'locales', - 'multilang', - 'requirements', - 'site', - 'slugs', - 'title', - 'translation', - 'user' => 'auth', - 'version' - ] - ], + if ($user && $user->role()->permissions()->for('access', 'system') === true) { + return $this->kirby()->version(); + } else { + return null; + } + } + ], + 'type' => 'Kirby\Cms\System', + 'views' => [ + 'login' => [ + 'authStatus', + 'isOk', + 'isInstallable', + 'isInstalled', + 'loginMethods', + 'title', + 'translation' + ], + 'troubleshooting' => [ + 'isOk', + 'isInstallable', + 'isInstalled', + 'title', + 'translation', + 'requirements' + ], + 'panel' => [ + 'ascii', + 'defaultLanguage', + 'isOk', + 'isInstalled', + 'isLocal', + 'kirbytext', + 'languages', + 'license', + 'locales', + 'multilang', + 'requirements', + 'site', + 'slugs', + 'title', + 'translation', + 'user' => 'auth', + 'version' + ] + ], ]; diff --git a/kirby/config/api/models/Translation.php b/kirby/config/api/models/Translation.php index fe31b56..faf51e7 100644 --- a/kirby/config/api/models/Translation.php +++ b/kirby/config/api/models/Translation.php @@ -6,19 +6,19 @@ use Kirby\Cms\Translation; * Translation */ return [ - 'fields' => [ - 'author' => fn (Translation $translation) => $translation->author(), - 'data' => fn (Translation $translation) => $translation->dataWithFallback(), - 'direction' => fn (Translation $translation) => $translation->direction(), - 'id' => fn (Translation $translation) => $translation->id(), - 'name' => fn (Translation $translation) => $translation->name(), - ], - 'type' => 'Kirby\Cms\Translation', - 'views' => [ - 'compact' => [ - 'direction', - 'id', - 'name' - ] - ] + 'fields' => [ + 'author' => fn (Translation $translation) => $translation->author(), + 'data' => fn (Translation $translation) => $translation->dataWithFallback(), + 'direction' => fn (Translation $translation) => $translation->direction(), + 'id' => fn (Translation $translation) => $translation->id(), + 'name' => fn (Translation $translation) => $translation->name(), + ], + 'type' => 'Kirby\Cms\Translation', + 'views' => [ + 'compact' => [ + 'direction', + 'id', + 'name' + ] + ] ]; diff --git a/kirby/config/api/models/User.php b/kirby/config/api/models/User.php index c8a7a5f..5354e3a 100644 --- a/kirby/config/api/models/User.php +++ b/kirby/config/api/models/User.php @@ -7,71 +7,71 @@ use Kirby\Form\Form; * User */ return [ - 'default' => fn () => $this->user(), - 'fields' => [ - 'avatar' => fn (User $user) => $user->avatar() ? $user->avatar()->crop(512) : null, - 'blueprint' => fn (User $user) => $user->blueprint(), - 'content' => fn (User $user) => Form::for($user)->values(), - 'email' => fn (User $user) => $user->email(), - 'files' => fn (User $user) => $user->files()->sorted(), - 'id' => fn (User $user) => $user->id(), - 'language' => fn (User $user) => $user->language(), - 'name' => fn (User $user) => $user->name()->value(), - 'next' => fn (User $user) => $user->next(), - 'options' => fn (User $user) => $user->panel()->options(), - 'panelImage' => fn (User $user) => $user->panel()->image(), - 'permissions' => fn (User $user) => $user->role()->permissions()->toArray(), - 'prev' => fn (User $user) => $user->prev(), - 'role' => fn (User $user) => $user->role(), - 'roles' => fn (User $user) => $user->roles(), - 'username' => fn (User $user) => $user->username() - ], - 'type' => 'Kirby\Cms\User', - 'views' => [ - 'default' => [ - 'avatar', - 'content', - 'email', - 'id', - 'language', - 'name', - 'next' => 'compact', - 'options', - 'prev' => 'compact', - 'role', - 'username' - ], - 'compact' => [ - 'avatar' => 'compact', - 'id', - 'email', - 'language', - 'name', - 'role' => 'compact', - 'username' - ], - 'auth' => [ - 'avatar' => 'compact', - 'permissions', - 'email', - 'id', - 'name', - 'role', - 'language' - ], - 'panel' => [ - 'avatar' => 'compact', - 'blueprint', - 'content', - 'email', - 'id', - 'language', - 'name', - 'next' => ['id', 'name'], - 'options', - 'prev' => ['id', 'name'], - 'role', - 'username', - ], - ] + 'default' => fn () => $this->user(), + 'fields' => [ + 'avatar' => fn (User $user) => $user->avatar() ? $user->avatar()->crop(512) : null, + 'blueprint' => fn (User $user) => $user->blueprint(), + 'content' => fn (User $user) => Form::for($user)->values(), + 'email' => fn (User $user) => $user->email(), + 'files' => fn (User $user) => $user->files()->sorted(), + 'id' => fn (User $user) => $user->id(), + 'language' => fn (User $user) => $user->language(), + 'name' => fn (User $user) => $user->name()->value(), + 'next' => fn (User $user) => $user->next(), + 'options' => fn (User $user) => $user->panel()->options(), + 'panelImage' => fn (User $user) => $user->panel()->image(), + 'permissions' => fn (User $user) => $user->role()->permissions()->toArray(), + 'prev' => fn (User $user) => $user->prev(), + 'role' => fn (User $user) => $user->role(), + 'roles' => fn (User $user) => $user->roles(), + 'username' => fn (User $user) => $user->username() + ], + 'type' => 'Kirby\Cms\User', + 'views' => [ + 'default' => [ + 'avatar', + 'content', + 'email', + 'id', + 'language', + 'name', + 'next' => 'compact', + 'options', + 'prev' => 'compact', + 'role', + 'username' + ], + 'compact' => [ + 'avatar' => 'compact', + 'id', + 'email', + 'language', + 'name', + 'role' => 'compact', + 'username' + ], + 'auth' => [ + 'avatar' => 'compact', + 'permissions', + 'email', + 'id', + 'name', + 'role', + 'language' + ], + 'panel' => [ + 'avatar' => 'compact', + 'blueprint', + 'content', + 'email', + 'id', + 'language', + 'name', + 'next' => ['id', 'name'], + 'options', + 'prev' => ['id', 'name'], + 'role', + 'username', + ], + ] ]; diff --git a/kirby/config/api/models/UserBlueprint.php b/kirby/config/api/models/UserBlueprint.php index f20c88a..099a177 100644 --- a/kirby/config/api/models/UserBlueprint.php +++ b/kirby/config/api/models/UserBlueprint.php @@ -6,13 +6,13 @@ use Kirby\Cms\UserBlueprint; * UserBlueprint */ return [ - 'fields' => [ - 'name' => fn (UserBlueprint $blueprint) => $blueprint->name(), - 'options' => fn (UserBlueprint $blueprint) => $blueprint->options(), - 'tabs' => fn (UserBlueprint $blueprint) => $blueprint->tabs(), - 'title' => fn (UserBlueprint $blueprint) => $blueprint->title(), - ], - 'type' => 'Kirby\Cms\UserBlueprint', - 'views' => [ - ], + 'fields' => [ + 'name' => fn (UserBlueprint $blueprint) => $blueprint->name(), + 'options' => fn (UserBlueprint $blueprint) => $blueprint->options(), + 'tabs' => fn (UserBlueprint $blueprint) => $blueprint->tabs(), + 'title' => fn (UserBlueprint $blueprint) => $blueprint->title(), + ], + 'type' => 'Kirby\Cms\UserBlueprint', + 'views' => [ + ], ]; diff --git a/kirby/config/api/routes.php b/kirby/config/api/routes.php index fd3449d..749fbad 100644 --- a/kirby/config/api/routes.php +++ b/kirby/config/api/routes.php @@ -4,23 +4,23 @@ * Api Routes Definitions */ return function ($kirby) { - $routes = array_merge( - include __DIR__ . '/routes/auth.php', - include __DIR__ . '/routes/pages.php', - include __DIR__ . '/routes/roles.php', - include __DIR__ . '/routes/site.php', - include __DIR__ . '/routes/users.php', - include __DIR__ . '/routes/files.php', - include __DIR__ . '/routes/lock.php', - include __DIR__ . '/routes/system.php', - include __DIR__ . '/routes/translations.php' - ); + $routes = array_merge( + include __DIR__ . '/routes/auth.php', + include __DIR__ . '/routes/pages.php', + include __DIR__ . '/routes/roles.php', + include __DIR__ . '/routes/site.php', + include __DIR__ . '/routes/users.php', + include __DIR__ . '/routes/files.php', + include __DIR__ . '/routes/lock.php', + include __DIR__ . '/routes/system.php', + include __DIR__ . '/routes/translations.php' + ); - // only add the language routes if the - // multi language setup is activated - if ($kirby->option('languages', false) !== false) { - $routes = array_merge($routes, include __DIR__ . '/routes/languages.php'); - } + // only add the language routes if the + // multi language setup is activated + if ($kirby->option('languages', false) !== false) { + $routes = array_merge($routes, include __DIR__ . '/routes/languages.php'); + } - return $routes; + return $routes; }; diff --git a/kirby/config/api/routes/auth.php b/kirby/config/api/routes/auth.php index 19f45a5..a4fcbb4 100644 --- a/kirby/config/api/routes/auth.php +++ b/kirby/config/api/routes/auth.php @@ -7,102 +7,102 @@ use Kirby\Exception\NotFoundException; * Authentication */ return [ - [ - 'pattern' => 'auth', - 'method' => 'GET', - 'action' => function () { - if ($user = $this->kirby()->auth()->user()) { - return $this->resolve($user)->view('auth'); - } + [ + 'pattern' => 'auth', + 'method' => 'GET', + 'action' => function () { + if ($user = $this->kirby()->auth()->user()) { + return $this->resolve($user)->view('auth'); + } - throw new NotFoundException('The user cannot be found'); - } - ], - [ - 'pattern' => 'auth/code', - 'method' => 'POST', - 'auth' => false, - 'action' => function () { - $auth = $this->kirby()->auth(); + throw new NotFoundException('The user cannot be found'); + } + ], + [ + 'pattern' => 'auth/code', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $auth = $this->kirby()->auth(); - // csrf token check - if ($auth->type() === 'session' && $auth->csrf() === false) { - throw new InvalidArgumentException('Invalid CSRF token'); - } + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new InvalidArgumentException('Invalid CSRF token'); + } - $user = $auth->verifyChallenge($this->requestBody('code')); + $user = $auth->verifyChallenge($this->requestBody('code')); - return [ - 'code' => 200, - 'status' => 'ok', - 'user' => $this->resolve($user)->view('auth')->toArray() - ]; - } - ], - [ - 'pattern' => 'auth/login', - 'method' => 'POST', - 'auth' => false, - 'action' => function () { - $auth = $this->kirby()->auth(); - $methods = $this->kirby()->system()->loginMethods(); + return [ + 'code' => 200, + 'status' => 'ok', + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } + ], + [ + 'pattern' => 'auth/login', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $auth = $this->kirby()->auth(); + $methods = $this->kirby()->system()->loginMethods(); - // csrf token check - if ($auth->type() === 'session' && $auth->csrf() === false) { - throw new InvalidArgumentException('Invalid CSRF token'); - } + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new InvalidArgumentException('Invalid CSRF token'); + } - $email = $this->requestBody('email'); - $long = $this->requestBody('long'); - $password = $this->requestBody('password'); + $email = $this->requestBody('email'); + $long = $this->requestBody('long'); + $password = $this->requestBody('password'); - if ($password) { - if (isset($methods['password']) !== true) { - throw new InvalidArgumentException('Login with password is not enabled'); - } + if ($password) { + if (isset($methods['password']) !== true) { + throw new InvalidArgumentException('Login with password is not enabled'); + } - if ( - isset($methods['password']['2fa']) === true && - $methods['password']['2fa'] === true - ) { - $status = $auth->login2fa($email, $password, $long); - } else { - $user = $auth->login($email, $password, $long); - } - } else { - if (isset($methods['code']) === true) { - $mode = 'login'; - } elseif (isset($methods['password-reset']) === true) { - $mode = 'password-reset'; - } else { - throw new InvalidArgumentException('Login without password is not enabled'); - } + if ( + isset($methods['password']['2fa']) === true && + $methods['password']['2fa'] === true + ) { + $status = $auth->login2fa($email, $password, $long); + } else { + $user = $auth->login($email, $password, $long); + } + } else { + if (isset($methods['code']) === true) { + $mode = 'login'; + } elseif (isset($methods['password-reset']) === true) { + $mode = 'password-reset'; + } else { + throw new InvalidArgumentException('Login without password is not enabled'); + } - $status = $auth->createChallenge($email, $long, $mode); - } + $status = $auth->createChallenge($email, $long, $mode); + } - if (isset($user)) { - return [ - 'code' => 200, - 'status' => 'ok', - 'user' => $this->resolve($user)->view('auth')->toArray() - ]; - } else { - return [ - 'code' => 200, - 'status' => 'ok', - 'challenge' => $status->challenge() - ]; - } - } - ], - [ - 'pattern' => 'auth/logout', - 'method' => 'POST', - 'auth' => false, - 'action' => function () { - $this->kirby()->auth()->logout(); - return true; - } - ], + if (isset($user)) { + return [ + 'code' => 200, + 'status' => 'ok', + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } else { + return [ + 'code' => 200, + 'status' => 'ok', + 'challenge' => $status->challenge() + ]; + } + } + ], + [ + 'pattern' => 'auth/logout', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $this->kirby()->auth()->logout(); + return true; + } + ], ]; diff --git a/kirby/config/api/routes/files.php b/kirby/config/api/routes/files.php index 77aea9c..c1b755b 100644 --- a/kirby/config/api/routes/files.php +++ b/kirby/config/api/routes/files.php @@ -8,125 +8,125 @@ $pattern = '(account|pages/[^/]+|site|users/[^/]+)'; */ return [ - [ - 'pattern' => $pattern . '/files/(:any)/sections/(:any)', - 'method' => 'GET', - 'action' => function (string $path, string $filename, string $sectionName) { - if ($section = $this->file($path, $filename)->blueprint()->section($sectionName)) { - return $section->toResponse(); - } - } - ], - [ - 'pattern' => $pattern . '/files/(:any)/fields/(:any)/(:all?)', - 'method' => 'ALL', - 'action' => function (string $parent, string $filename, string $fieldName, string $path = null) { - if ($file = $this->file($parent, $filename)) { - return $this->fieldApi($file, $fieldName, $path); - } - } - ], - [ - 'pattern' => $pattern . '/files', - 'method' => 'GET', - 'action' => function (string $path) { - return $this->parent($path)->files()->sorted(); - } - ], - [ - 'pattern' => $pattern . '/files', - 'method' => 'POST', - 'action' => function (string $path) { - // move_uploaded_file() not working with unit test - // @codeCoverageIgnoreStart - return $this->upload(function ($source, $filename) use ($path) { - return $this->parent($path)->createFile([ - 'content' => [ - 'sort' => $this->requestBody('sort') - ], - 'source' => $source, - 'template' => $this->requestBody('template'), - 'filename' => $filename - ]); - }); - // @codeCoverageIgnoreEnd - } - ], - [ - 'pattern' => $pattern . '/files/search', - 'method' => 'GET|POST', - 'action' => function (string $path) { - $files = $this->parent($path)->files(); + [ + 'pattern' => $pattern . '/files/(:any)/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $path, string $filename, string $sectionName) { + if ($section = $this->file($path, $filename)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => $pattern . '/files/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $parent, string $filename, string $fieldName, string $path = null) { + if ($file = $this->file($parent, $filename)) { + return $this->fieldApi($file, $fieldName, $path); + } + } + ], + [ + 'pattern' => $pattern . '/files', + 'method' => 'GET', + 'action' => function (string $path) { + return $this->parent($path)->files()->sorted(); + } + ], + [ + 'pattern' => $pattern . '/files', + 'method' => 'POST', + 'action' => function (string $path) { + // move_uploaded_file() not working with unit test + // @codeCoverageIgnoreStart + return $this->upload(function ($source, $filename) use ($path) { + return $this->parent($path)->createFile([ + 'content' => [ + 'sort' => $this->requestBody('sort') + ], + 'source' => $source, + 'template' => $this->requestBody('template'), + 'filename' => $filename + ]); + }); + // @codeCoverageIgnoreEnd + } + ], + [ + 'pattern' => $pattern . '/files/search', + 'method' => 'GET|POST', + 'action' => function (string $path) { + $files = $this->parent($path)->files(); - if ($this->requestMethod() === 'GET') { - return $files->search($this->requestQuery('q')); - } else { - return $files->query($this->requestBody()); - } - } - ], - [ - 'pattern' => $pattern . '/files/sort', - 'method' => 'PATCH', - 'action' => function (string $path) { - return $this->parent($path)->files()->changeSort( - $this->requestBody('files'), - $this->requestBody('index') - ); - } - ], - [ - 'pattern' => $pattern . '/files/(:any)', - 'method' => 'GET', - 'action' => function (string $path, string $filename) { - return $this->file($path, $filename); - } - ], - [ - 'pattern' => $pattern . '/files/(:any)', - 'method' => 'PATCH', - 'action' => function (string $path, string $filename) { - return $this->file($path, $filename)->update($this->requestBody(), $this->language(), true); - } - ], - [ - 'pattern' => $pattern . '/files/(:any)', - 'method' => 'POST', - 'action' => function (string $path, string $filename) { - return $this->upload(function ($source) use ($path, $filename) { - return $this->file($path, $filename)->replace($source); - }); - } - ], - [ - 'pattern' => $pattern . '/files/(:any)', - 'method' => 'DELETE', - 'action' => function (string $path, string $filename) { - return $this->file($path, $filename)->delete(); - } - ], - [ - 'pattern' => $pattern . '/files/(:any)/name', - 'method' => 'PATCH', - 'action' => function (string $path, string $filename) { - return $this->file($path, $filename)->changeName($this->requestBody('name')); - } - ], - [ - 'pattern' => 'files/search', - 'method' => 'GET|POST', - 'action' => function () { - $files = $this - ->site() - ->index(true) - ->filter('isReadable', true) - ->files(); + if ($this->requestMethod() === 'GET') { + return $files->search($this->requestQuery('q')); + } else { + return $files->query($this->requestBody()); + } + } + ], + [ + 'pattern' => $pattern . '/files/sort', + 'method' => 'PATCH', + 'action' => function (string $path) { + return $this->parent($path)->files()->changeSort( + $this->requestBody('files'), + $this->requestBody('index') + ); + } + ], + [ + 'pattern' => $pattern . '/files/(:any)', + 'method' => 'GET', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename); + } + ], + [ + 'pattern' => $pattern . '/files/(:any)', + 'method' => 'PATCH', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => $pattern . '/files/(:any)', + 'method' => 'POST', + 'action' => function (string $path, string $filename) { + return $this->upload(function ($source) use ($path, $filename) { + return $this->file($path, $filename)->replace($source); + }); + } + ], + [ + 'pattern' => $pattern . '/files/(:any)', + 'method' => 'DELETE', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->delete(); + } + ], + [ + 'pattern' => $pattern . '/files/(:any)/name', + 'method' => 'PATCH', + 'action' => function (string $path, string $filename) { + return $this->file($path, $filename)->changeName($this->requestBody('name')); + } + ], + [ + 'pattern' => 'files/search', + 'method' => 'GET|POST', + 'action' => function () { + $files = $this + ->site() + ->index(true) + ->filter('isReadable', true) + ->files(); - if ($this->requestMethod() === 'GET') { - return $files->search($this->requestQuery('q')); - } else { - return $files->query($this->requestBody()); - } - } - ], + if ($this->requestMethod() === 'GET') { + return $files->search($this->requestQuery('q')); + } else { + return $files->query($this->requestBody()); + } + } + ], ]; diff --git a/kirby/config/api/routes/languages.php b/kirby/config/api/routes/languages.php index 8d8829b..4105ef2 100644 --- a/kirby/config/api/routes/languages.php +++ b/kirby/config/api/routes/languages.php @@ -4,43 +4,43 @@ * Roles Routes */ return [ - [ - 'pattern' => 'languages', - 'method' => 'GET', - 'action' => function () { - return $this->kirby()->languages(); - } - ], - [ - 'pattern' => 'languages', - 'method' => 'POST', - 'action' => function () { - return $this->kirby()->languages()->create($this->requestBody()); - } - ], - [ - 'pattern' => 'languages/(:any)', - 'method' => 'GET', - 'action' => function (string $code) { - return $this->kirby()->languages()->find($code); - } - ], - [ - 'pattern' => 'languages/(:any)', - 'method' => 'PATCH', - 'action' => function (string $code) { - if ($language = $this->kirby()->languages()->find($code)) { - return $language->update($this->requestBody()); - } - } - ], - [ - 'pattern' => 'languages/(:any)', - 'method' => 'DELETE', - 'action' => function (string $code) { - if ($language = $this->kirby()->languages()->find($code)) { - return $language->delete(); - } - } - ] + [ + 'pattern' => 'languages', + 'method' => 'GET', + 'action' => function () { + return $this->kirby()->languages(); + } + ], + [ + 'pattern' => 'languages', + 'method' => 'POST', + 'action' => function () { + return $this->kirby()->languages()->create($this->requestBody()); + } + ], + [ + 'pattern' => 'languages/(:any)', + 'method' => 'GET', + 'action' => function (string $code) { + return $this->kirby()->languages()->find($code); + } + ], + [ + 'pattern' => 'languages/(:any)', + 'method' => 'PATCH', + 'action' => function (string $code) { + if ($language = $this->kirby()->languages()->find($code)) { + return $language->update($this->requestBody()); + } + } + ], + [ + 'pattern' => 'languages/(:any)', + 'method' => 'DELETE', + 'action' => function (string $code) { + if ($language = $this->kirby()->languages()->find($code)) { + return $language->delete(); + } + } + ] ]; diff --git a/kirby/config/api/routes/lock.php b/kirby/config/api/routes/lock.php index bbe9bad..c5b2da4 100644 --- a/kirby/config/api/routes/lock.php +++ b/kirby/config/api/routes/lock.php @@ -5,87 +5,40 @@ * Content Lock Routes */ return [ - [ - 'pattern' => '(:all)/lock', - 'method' => 'GET', - /** - * @deprecated 3.6.0 - * @todo Remove in 3.7.0 - */ - 'action' => function (string $path) { - deprecated('The `GET (:all)/lock` API endpoint has been deprecated and will be removed in 3.7.0'); - - if ($lock = $this->parent($path)->lock()) { - return [ - 'supported' => true, - 'locked' => $lock->get() - ]; - } - - return [ - 'supported' => false, - 'locked' => null - ]; - } - ], - [ - 'pattern' => '(:all)/lock', - 'method' => 'PATCH', - 'action' => function (string $path) { - if ($lock = $this->parent($path)->lock()) { - return $lock->create(); - } - } - ], - [ - 'pattern' => '(:all)/lock', - 'method' => 'DELETE', - 'action' => function (string $path) { - if ($lock = $this->parent($path)->lock()) { - return $lock->remove(); - } - } - ], - [ - 'pattern' => '(:all)/unlock', - 'method' => 'GET', - /** - * @deprecated 3.6.0 - * @todo Remove in 3.7.0 - */ - 'action' => function (string $path) { - deprecated('The `GET (:all)/unlock` API endpoint has been deprecated and will be removed in 3.7.0'); - - - if ($lock = $this->parent($path)->lock()) { - return [ - 'supported' => true, - 'unlocked' => $lock->isUnlocked() - ]; - } - - return [ - 'supported' => false, - 'unlocked' => null - ]; - } - ], - [ - 'pattern' => '(:all)/unlock', - 'method' => 'PATCH', - 'action' => function (string $path) { - if ($lock = $this->parent($path)->lock()) { - return $lock->unlock(); - } - } - ], - [ - 'pattern' => '(:all)/unlock', - 'method' => 'DELETE', - 'action' => function (string $path) { - if ($lock = $this->parent($path)->lock()) { - return $lock->resolve(); - } - } - ], + [ + 'pattern' => '(:all)/lock', + 'method' => 'PATCH', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->create(); + } + } + ], + [ + 'pattern' => '(:all)/lock', + 'method' => 'DELETE', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->remove(); + } + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'PATCH', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->unlock(); + } + } + ], + [ + 'pattern' => '(:all)/unlock', + 'method' => 'DELETE', + 'action' => function (string $path) { + if ($lock = $this->parent($path)->lock()) { + return $lock->resolve(); + } + } + ], ]; diff --git a/kirby/config/api/routes/pages.php b/kirby/config/api/routes/pages.php index 247f970..3ff255c 100644 --- a/kirby/config/api/routes/pages.php +++ b/kirby/config/api/routes/pages.php @@ -1,132 +1,121 @@ 'pages/(:any)', - 'method' => 'GET', - 'action' => function (string $id) { - return $this->page($id); - } - ], - [ - 'pattern' => 'pages/(:any)', - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->page($id)->update($this->requestBody(), $this->language(), true); - } - ], - [ - 'pattern' => 'pages/(:any)', - 'method' => 'DELETE', - 'action' => function (string $id) { - return $this->page($id)->delete($this->requestBody('force', false)); - } - ], - [ - 'pattern' => 'pages/(:any)/blueprint', - 'method' => 'GET', - 'action' => function (string $id) { - return $this->page($id)->blueprint(); - } - ], - [ - 'pattern' => [ - 'pages/(:any)/blueprints', - /** - * @deprecated - * @todo remove in 3.7.0 - */ - 'pages/(:any)/children/blueprints', - ], - 'method' => 'GET', - 'action' => function (string $id) { - // @codeCoverageIgnoreStart - if ($this->route->pattern() === 'pages/([a-zA-Z0-9\.\-_%= \+\@\(\)]+)/children/blueprints') { - deprecated('`GET pages/(:any)/children/blueprints` API endpoint has been deprecated and will be removed in 3.7.0. Use `GET pages/(:any)/blueprints` instead'); - } - // @codeCoverageIgnoreEnd - return $this->page($id)->blueprints($this->requestQuery('section')); - } - ], - [ - 'pattern' => 'pages/(:any)/children', - 'method' => 'GET', - 'action' => function (string $id) { - return $this->pages($id, $this->requestQuery('status')); - } - ], - [ - 'pattern' => 'pages/(:any)/children', - 'method' => 'POST', - 'action' => function (string $id) { - return $this->page($id)->createChild($this->requestBody()); - } - ], - [ - 'pattern' => 'pages/(:any)/children/search', - 'method' => 'GET|POST', - 'action' => function (string $id) { - return $this->searchPages($id); - } - ], - [ - 'pattern' => 'pages/(:any)/duplicate', - 'method' => 'POST', - 'action' => function (string $id) { - return $this->page($id)->duplicate($this->requestBody('slug'), [ - 'children' => $this->requestBody('children'), - 'files' => $this->requestBody('files'), - ]); - } - ], - [ - 'pattern' => 'pages/(:any)/slug', - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->page($id)->changeSlug($this->requestBody('slug')); - } - ], - [ - 'pattern' => 'pages/(:any)/status', - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->page($id)->changeStatus($this->requestBody('status'), $this->requestBody('position')); - } - ], - [ - 'pattern' => 'pages/(:any)/template', - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->page($id)->changeTemplate($this->requestBody('template')); - } - ], - [ - 'pattern' => 'pages/(:any)/title', - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->page($id)->changeTitle($this->requestBody('title')); - } - ], - [ - 'pattern' => 'pages/(:any)/sections/(:any)', - 'method' => 'GET', - 'action' => function (string $id, string $sectionName) { - if ($section = $this->page($id)->blueprint()->section($sectionName)) { - return $section->toResponse(); - } - } - ], - [ - 'pattern' => 'pages/(:any)/fields/(:any)/(:all?)', - 'method' => 'ALL', - 'action' => function (string $id, string $fieldName, string $path = null) { - if ($page = $this->page($id)) { - return $this->fieldApi($page, $fieldName, $path); - } - } - ], + [ + 'pattern' => 'pages/(:any)', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id); + } + ], + [ + 'pattern' => 'pages/(:any)', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => 'pages/(:any)', + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->page($id)->delete($this->requestBody('force', false)); + } + ], + [ + 'pattern' => 'pages/(:any)/blueprint', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id)->blueprint(); + } + ], + [ + 'pattern' => 'pages/(:any)/blueprints', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->page($id)->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'pages/(:any)/children', + 'method' => 'GET', + 'action' => function (string $id) { + return $this->pages($id, $this->requestQuery('status')); + } + ], + [ + 'pattern' => 'pages/(:any)/children', + 'method' => 'POST', + 'action' => function (string $id) { + return $this->page($id)->createChild($this->requestBody()); + } + ], + [ + 'pattern' => 'pages/(:any)/children/search', + 'method' => 'GET|POST', + 'action' => function (string $id) { + return $this->searchPages($id); + } + ], + [ + 'pattern' => 'pages/(:any)/duplicate', + 'method' => 'POST', + 'action' => function (string $id) { + return $this->page($id)->duplicate($this->requestBody('slug'), [ + 'children' => $this->requestBody('children'), + 'files' => $this->requestBody('files'), + ]); + } + ], + [ + 'pattern' => 'pages/(:any)/slug', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeSlug($this->requestBody('slug')); + } + ], + [ + 'pattern' => 'pages/(:any)/status', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeStatus($this->requestBody('status'), $this->requestBody('position')); + } + ], + [ + 'pattern' => 'pages/(:any)/template', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeTemplate($this->requestBody('template')); + } + ], + [ + 'pattern' => 'pages/(:any)/title', + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->page($id)->changeTitle($this->requestBody('title')); + } + ], + [ + 'pattern' => 'pages/(:any)/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $id, string $sectionName) { + if ($section = $this->page($id)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => 'pages/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $id, string $fieldName, string $path = null) { + if ($page = $this->page($id)) { + return $this->fieldApi($page, $fieldName, $path); + } + } + ], ]; diff --git a/kirby/config/api/routes/roles.php b/kirby/config/api/routes/roles.php index ab9505b..7dbe45f 100644 --- a/kirby/config/api/routes/roles.php +++ b/kirby/config/api/routes/roles.php @@ -4,25 +4,27 @@ * Roles Routes */ return [ - [ - 'pattern' => 'roles', - 'method' => 'GET', - 'action' => function () { - switch (get('canBe')) { - case 'changed': - return $this->kirby()->roles()->canBeChanged(); - case 'created': - return $this->kirby()->roles()->canBeCreated(); - default: - return $this->kirby()->roles(); - } - } - ], - [ - 'pattern' => 'roles/(:any)', - 'method' => 'GET', - 'action' => function (string $name) { - return $this->kirby()->roles()->find($name); - } - ] + [ + 'pattern' => 'roles', + 'method' => 'GET', + 'action' => function () { + $kirby = $this->kirby(); + + switch ($kirby->request()->get('canBe')) { + case 'changed': + return $kirby->roles()->canBeChanged(); + case 'created': + return $kirby->roles()->canBeCreated(); + default: + return $kirby->roles(); + } + } + ], + [ + 'pattern' => 'roles/(:any)', + 'method' => 'GET', + 'action' => function (string $name) { + return $this->kirby()->roles()->find($name); + } + ] ]; diff --git a/kirby/config/api/routes/site.php b/kirby/config/api/routes/site.php index 59e8cab..3f2601c 100644 --- a/kirby/config/api/routes/site.php +++ b/kirby/config/api/routes/site.php @@ -1,115 +1,104 @@ 'site', - 'action' => function () { - return $this->site(); - } - ], - [ - 'pattern' => 'site', - 'method' => 'PATCH', - 'action' => function () { - return $this->site()->update($this->requestBody(), $this->language(), true); - } - ], - [ - 'pattern' => 'site/children', - 'method' => 'GET', - 'action' => function () { - return $this->pages(null, $this->requestQuery('status')); - } - ], - [ - 'pattern' => 'site/children', - 'method' => 'POST', - 'action' => function () { - return $this->site()->createChild($this->requestBody()); - } - ], - [ - 'pattern' => 'site/children/search', - 'method' => 'GET|POST', - 'action' => function () { - return $this->searchPages(); - } - ], - [ - 'pattern' => 'site/blueprint', - 'method' => 'GET', - 'action' => function () { - return $this->site()->blueprint(); - } - ], - [ - 'pattern' => [ - 'site/blueprints', - /** - * @deprecated - * @todo remove in 3.7.0 - */ - 'site/children/blueprints', - ], - 'method' => 'GET', - 'action' => function () { - // @codeCoverageIgnoreStart - if ($this->route->pattern() === 'site/children/blueprints') { - deprecated('`GET site/children/blueprints` API endpoint has been deprecated and will be removed in 3.7.0. Use `GET site/blueprints` instead.'); - } - // @codeCoverageIgnoreEnd - return $this->site()->blueprints($this->requestQuery('section')); - } - ], - [ - 'pattern' => 'site/find', - 'method' => 'POST', - 'action' => function () { - return $this->site()->find(false, ...$this->requestBody()); - } - ], - [ - 'pattern' => 'site/title', - 'method' => 'PATCH', - 'action' => function () { - return $this->site()->changeTitle($this->requestBody('title')); - } - ], - [ - 'pattern' => 'site/search', - 'method' => 'GET|POST', - 'action' => function () { - $pages = $this - ->site() - ->index(true) - ->filter('isReadable', true); + [ + 'pattern' => 'site', + 'action' => function () { + return $this->site(); + } + ], + [ + 'pattern' => 'site', + 'method' => 'PATCH', + 'action' => function () { + return $this->site()->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => 'site/children', + 'method' => 'GET', + 'action' => function () { + return $this->pages(null, $this->requestQuery('status')); + } + ], + [ + 'pattern' => 'site/children', + 'method' => 'POST', + 'action' => function () { + return $this->site()->createChild($this->requestBody()); + } + ], + [ + 'pattern' => 'site/children/search', + 'method' => 'GET|POST', + 'action' => function () { + return $this->searchPages(); + } + ], + [ + 'pattern' => 'site/blueprint', + 'method' => 'GET', + 'action' => function () { + return $this->site()->blueprint(); + } + ], + [ + 'pattern' => 'site/blueprints', + 'method' => 'GET', + 'action' => function () { + return $this->site()->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => 'site/find', + 'method' => 'POST', + 'action' => function () { + return $this->site()->find(false, ...$this->requestBody()); + } + ], + [ + 'pattern' => 'site/title', + 'method' => 'PATCH', + 'action' => function () { + return $this->site()->changeTitle($this->requestBody('title')); + } + ], + [ + 'pattern' => 'site/search', + 'method' => 'GET|POST', + 'action' => function () { + $pages = $this + ->site() + ->index(true) + ->filter('isReadable', true); - if ($this->requestMethod() === 'GET') { - return $pages->search($this->requestQuery('q')); - } else { - return $pages->query($this->requestBody()); - } - } - ], - [ - 'pattern' => 'site/sections/(:any)', - 'method' => 'GET', - 'action' => function (string $sectionName) { - if ($section = $this->site()->blueprint()->section($sectionName)) { - return $section->toResponse(); - } - } - ], - [ - 'pattern' => 'site/fields/(:any)/(:all?)', - 'method' => 'ALL', - 'action' => function (string $fieldName, string $path = null) { - return $this->fieldApi($this->site(), $fieldName, $path); - } - ] + if ($this->requestMethod() === 'GET') { + return $pages->search($this->requestQuery('q')); + } else { + return $pages->query($this->requestBody()); + } + } + ], + [ + 'pattern' => 'site/sections/(:any)', + 'method' => 'GET', + 'action' => function (string $sectionName) { + if ($section = $this->site()->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => 'site/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $fieldName, string $path = null) { + return $this->fieldApi($this->site(), $fieldName, $path); + } + ] ]; diff --git a/kirby/config/api/routes/system.php b/kirby/config/api/routes/system.php index 44c8807..0f6ba46 100644 --- a/kirby/config/api/routes/system.php +++ b/kirby/config/api/routes/system.php @@ -8,72 +8,72 @@ use Kirby\Exception\InvalidArgumentException; */ return [ - [ - 'pattern' => 'system', - 'method' => 'GET', - 'auth' => false, - 'action' => function () { - $system = $this->kirby()->system(); + [ + 'pattern' => 'system', + 'method' => 'GET', + 'auth' => false, + 'action' => function () { + $system = $this->kirby()->system(); - if ($this->kirby()->user()) { - return $system; - } else { - if ($system->isOk() === true) { - $info = $this->resolve($system)->view('login')->toArray(); - } else { - $info = $this->resolve($system)->view('troubleshooting')->toArray(); - } + if ($this->kirby()->user()) { + return $system; + } else { + if ($system->isOk() === true) { + $info = $this->resolve($system)->view('login')->toArray(); + } else { + $info = $this->resolve($system)->view('troubleshooting')->toArray(); + } - return [ - 'status' => 'ok', - 'data' => $info, - 'type' => 'model' - ]; - } - } - ], - [ - 'pattern' => 'system/register', - 'method' => 'POST', - 'action' => function () { - return $this->kirby()->system()->register($this->requestBody('license'), $this->requestBody('email')); - } - ], - [ - 'pattern' => 'system/install', - 'method' => 'POST', - 'auth' => false, - 'action' => function () { - $system = $this->kirby()->system(); - $auth = $this->kirby()->auth(); + return [ + 'status' => 'ok', + 'data' => $info, + 'type' => 'model' + ]; + } + } + ], + [ + 'pattern' => 'system/register', + 'method' => 'POST', + 'action' => function () { + return $this->kirby()->system()->register($this->requestBody('license'), $this->requestBody('email')); + } + ], + [ + 'pattern' => 'system/install', + 'method' => 'POST', + 'auth' => false, + 'action' => function () { + $system = $this->kirby()->system(); + $auth = $this->kirby()->auth(); - // csrf token check - if ($auth->type() === 'session' && $auth->csrf() === false) { - throw new InvalidArgumentException('Invalid CSRF token'); - } + // csrf token check + if ($auth->type() === 'session' && $auth->csrf() === false) { + throw new InvalidArgumentException('Invalid CSRF token'); + } - if ($system->isOk() === false) { - throw new Exception('The server is not setup correctly'); - } + if ($system->isOk() === false) { + throw new Exception('The server is not setup correctly'); + } - if ($system->isInstallable() === false) { - throw new Exception('The Panel cannot be installed'); - } + if ($system->isInstallable() === false) { + throw new Exception('The Panel cannot be installed'); + } - if ($system->isInstalled() === true) { - throw new Exception('The Panel is already installed'); - } + if ($system->isInstalled() === true) { + throw new Exception('The Panel is already installed'); + } - // create the first user - $user = $this->users()->create($this->requestBody()); - $token = $user->login($this->requestBody('password')); + // create the first user + $user = $this->users()->create($this->requestBody()); + $token = $user->login($this->requestBody('password')); - return [ - 'status' => 'ok', - 'token' => $token, - 'user' => $this->resolve($user)->view('auth')->toArray() - ]; - } - ] + return [ + 'status' => 'ok', + 'token' => $token, + 'user' => $this->resolve($user)->view('auth')->toArray() + ]; + } + ] ]; diff --git a/kirby/config/api/routes/translations.php b/kirby/config/api/routes/translations.php index db7faca..2d949c5 100644 --- a/kirby/config/api/routes/translations.php +++ b/kirby/config/api/routes/translations.php @@ -4,21 +4,21 @@ * Translations Routes */ return [ - [ - 'pattern' => 'translations', - 'method' => 'GET', - 'auth' => false, - 'action' => function () { - return $this->kirby()->translations(); - } - ], - [ - 'pattern' => 'translations/(:any)', - 'method' => 'GET', - 'auth' => false, - 'action' => function (string $code) { - return $this->kirby()->translations()->find($code); - } - ] + [ + 'pattern' => 'translations', + 'method' => 'GET', + 'auth' => false, + 'action' => function () { + return $this->kirby()->translations(); + } + ], + [ + 'pattern' => 'translations/(:any)', + 'method' => 'GET', + 'auth' => false, + 'action' => function (string $code) { + return $this->kirby()->translations()->find($code); + } + ] ]; diff --git a/kirby/config/api/routes/users.php b/kirby/config/api/routes/users.php index abd09c5..daa245d 100644 --- a/kirby/config/api/routes/users.php +++ b/kirby/config/api/routes/users.php @@ -6,202 +6,202 @@ use Kirby\Filesystem\F; * User Routes */ return [ - [ - 'pattern' => 'users', - 'method' => 'GET', - 'action' => function () { - return $this->users()->sort('username', 'asc', 'email', 'asc'); - } - ], - [ - 'pattern' => 'users', - 'method' => 'POST', - 'action' => function () { - return $this->users()->create($this->requestBody()); - } - ], - [ - 'pattern' => 'users/search', - 'method' => 'GET|POST', - 'action' => function () { - if ($this->requestMethod() === 'GET') { - return $this->users()->search($this->requestQuery('q')); - } else { - return $this->users()->query($this->requestBody()); - } - } - ], - [ - 'pattern' => [ - '(account)', - 'users/(:any)', - ], - 'method' => 'GET', - 'action' => function (string $id) { - return $this->user($id); - } - ], - [ - 'pattern' => [ - '(account)', - 'users/(:any)', - ], - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->user($id)->update($this->requestBody(), $this->language(), true); - } - ], - [ - 'pattern' => [ - '(account)', - 'users/(:any)', - ], - 'method' => 'DELETE', - 'action' => function (string $id) { - return $this->user($id)->delete(); - } - ], - [ - 'pattern' => [ - '(account)/avatar', - 'users/(:any)/avatar', - ], - 'method' => 'GET', - 'action' => function (string $id) { - return $this->user($id)->avatar(); - } - ], - // @codeCoverageIgnoreStart - [ - 'pattern' => [ - '(account)/avatar', - 'users/(:any)/avatar', - ], - 'method' => 'POST', - 'action' => function (string $id) { - if ($avatar = $this->user($id)->avatar()) { - $avatar->delete(); - } + [ + 'pattern' => 'users', + 'method' => 'GET', + 'action' => function () { + return $this->users()->sort('username', 'asc', 'email', 'asc'); + } + ], + [ + 'pattern' => 'users', + 'method' => 'POST', + 'action' => function () { + return $this->users()->create($this->requestBody()); + } + ], + [ + 'pattern' => 'users/search', + 'method' => 'GET|POST', + 'action' => function () { + if ($this->requestMethod() === 'GET') { + return $this->users()->search($this->requestQuery('q')); + } else { + return $this->users()->query($this->requestBody()); + } + } + ], + [ + 'pattern' => [ + '(account)', + 'users/(:any)', + ], + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id); + } + ], + [ + 'pattern' => [ + '(account)', + 'users/(:any)', + ], + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->update($this->requestBody(), $this->language(), true); + } + ], + [ + 'pattern' => [ + '(account)', + 'users/(:any)', + ], + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->user($id)->delete(); + } + ], + [ + 'pattern' => [ + '(account)/avatar', + 'users/(:any)/avatar', + ], + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->avatar(); + } + ], + // @codeCoverageIgnoreStart + [ + 'pattern' => [ + '(account)/avatar', + 'users/(:any)/avatar', + ], + 'method' => 'POST', + 'action' => function (string $id) { + if ($avatar = $this->user($id)->avatar()) { + $avatar->delete(); + } - return $this->upload(function ($source, $filename) use ($id) { - return $this->user($id)->createFile([ - 'filename' => 'profile.' . F::extension($filename), - 'template' => 'avatar', - 'source' => $source - ]); - }, $single = true); - } - ], - // @codeCoverageIgnoreEnd - [ - 'pattern' => [ - '(account)/avatar', - 'users/(:any)/avatar', - ], - 'method' => 'DELETE', - 'action' => function (string $id) { - return $this->user($id)->avatar()->delete(); - } - ], - [ - 'pattern' => [ - '(account)/blueprint', - 'users/(:any)/blueprint', - ], - 'method' => 'GET', - 'action' => function (string $id) { - return $this->user($id)->blueprint(); - } - ], - [ - 'pattern' => [ - '(account)/blueprints', - 'users/(:any)/blueprints', - ], - 'method' => 'GET', - 'action' => function (string $id) { - return $this->user($id)->blueprints($this->requestQuery('section')); - } - ], - [ - 'pattern' => [ - '(account)/email', - 'users/(:any)/email', - ], - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->user($id)->changeEmail($this->requestBody('email')); - } - ], - [ - 'pattern' => [ - '(account)/language', - 'users/(:any)/language', - ], - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->user($id)->changeLanguage($this->requestBody('language')); - } - ], - [ - 'pattern' => [ - '(account)/name', - 'users/(:any)/name', - ], - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->user($id)->changeName($this->requestBody('name')); - } - ], - [ - 'pattern' => [ - '(account)/password', - 'users/(:any)/password', - ], - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->user($id)->changePassword($this->requestBody('password')); - } - ], - [ - 'pattern' => [ - '(account)/role', - 'users/(:any)/role', - ], - 'method' => 'PATCH', - 'action' => function (string $id) { - return $this->user($id)->changeRole($this->requestBody('role')); - } - ], - [ - 'pattern' => [ - '(account)/roles', - 'users/(:any)/roles', - ], - 'action' => function (string $id) { - return $this->user($id)->roles(); - } - ], - [ - 'pattern' => [ - '(account)/sections/(:any)', - 'users/(:any)/sections/(:any)', - ], - 'method' => 'GET', - 'action' => function (string $id, string $sectionName) { - if ($section = $this->user($id)->blueprint()->section($sectionName)) { - return $section->toResponse(); - } - } - ], - [ - 'pattern' => [ - '(account)/fields/(:any)/(:all?)', - 'users/(:any)/fields/(:any)/(:all?)', - ], - 'method' => 'ALL', - 'action' => function (string $id, string $fieldName, string $path = null) { - return $this->fieldApi($this->user($id), $fieldName, $path); - } - ], + return $this->upload(function ($source, $filename) use ($id) { + return $this->user($id)->createFile([ + 'filename' => 'profile.' . F::extension($filename), + 'template' => 'avatar', + 'source' => $source + ]); + }, $single = true); + } + ], + // @codeCoverageIgnoreEnd + [ + 'pattern' => [ + '(account)/avatar', + 'users/(:any)/avatar', + ], + 'method' => 'DELETE', + 'action' => function (string $id) { + return $this->user($id)->avatar()->delete(); + } + ], + [ + 'pattern' => [ + '(account)/blueprint', + 'users/(:any)/blueprint', + ], + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->blueprint(); + } + ], + [ + 'pattern' => [ + '(account)/blueprints', + 'users/(:any)/blueprints', + ], + 'method' => 'GET', + 'action' => function (string $id) { + return $this->user($id)->blueprints($this->requestQuery('section')); + } + ], + [ + 'pattern' => [ + '(account)/email', + 'users/(:any)/email', + ], + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeEmail($this->requestBody('email')); + } + ], + [ + 'pattern' => [ + '(account)/language', + 'users/(:any)/language', + ], + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeLanguage($this->requestBody('language')); + } + ], + [ + 'pattern' => [ + '(account)/name', + 'users/(:any)/name', + ], + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeName($this->requestBody('name')); + } + ], + [ + 'pattern' => [ + '(account)/password', + 'users/(:any)/password', + ], + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changePassword($this->requestBody('password')); + } + ], + [ + 'pattern' => [ + '(account)/role', + 'users/(:any)/role', + ], + 'method' => 'PATCH', + 'action' => function (string $id) { + return $this->user($id)->changeRole($this->requestBody('role')); + } + ], + [ + 'pattern' => [ + '(account)/roles', + 'users/(:any)/roles', + ], + 'action' => function (string $id) { + return $this->user($id)->roles(); + } + ], + [ + 'pattern' => [ + '(account)/sections/(:any)', + 'users/(:any)/sections/(:any)', + ], + 'method' => 'GET', + 'action' => function (string $id, string $sectionName) { + if ($section = $this->user($id)->blueprint()->section($sectionName)) { + return $section->toResponse(); + } + } + ], + [ + 'pattern' => [ + '(account)/fields/(:any)/(:all?)', + 'users/(:any)/fields/(:any)/(:all?)', + ], + 'method' => 'ALL', + 'action' => function (string $id, string $fieldName, string $path = null) { + return $this->fieldApi($this->user($id), $fieldName, $path); + } + ], ]; diff --git a/kirby/config/areas/account.php b/kirby/config/areas/account.php index b2f629f..738a839 100644 --- a/kirby/config/areas/account.php +++ b/kirby/config/areas/account.php @@ -1,12 +1,14 @@ 'account', - 'label' => t('view.account'), - 'search' => 'users', - 'dialogs' => require __DIR__ . '/account/dialogs.php', - 'dropdowns' => require __DIR__ . '/account/dropdowns.php', - 'views' => require __DIR__ . '/account/views.php' - ]; + return [ + 'icon' => 'account', + 'label' => I18n::translate('view.account'), + 'search' => 'users', + 'dialogs' => require __DIR__ . '/account/dialogs.php', + 'dropdowns' => require __DIR__ . '/account/dropdowns.php', + 'views' => require __DIR__ . '/account/views.php' + ]; }; diff --git a/kirby/config/areas/account/dialogs.php b/kirby/config/areas/account/dialogs.php index 15bf7b7..7c13062 100644 --- a/kirby/config/areas/account/dialogs.php +++ b/kirby/config/areas/account/dialogs.php @@ -4,67 +4,67 @@ $dialogs = require __DIR__ . '/../users/dialogs.php'; return [ - // change email - 'account.changeEmail' => [ - 'pattern' => '(account)/changeEmail', - 'load' => $dialogs['user.changeEmail']['load'], - 'submit' => $dialogs['user.changeEmail']['submit'], - ], + // change email + 'account.changeEmail' => [ + 'pattern' => '(account)/changeEmail', + 'load' => $dialogs['user.changeEmail']['load'], + 'submit' => $dialogs['user.changeEmail']['submit'], + ], - // change language - 'account.changeLanguage' => [ - 'pattern' => '(account)/changeLanguage', - 'load' => $dialogs['user.changeLanguage']['load'], - 'submit' => $dialogs['user.changeLanguage']['submit'], - ], + // change language + 'account.changeLanguage' => [ + 'pattern' => '(account)/changeLanguage', + 'load' => $dialogs['user.changeLanguage']['load'], + 'submit' => $dialogs['user.changeLanguage']['submit'], + ], - // change name - 'account.changeName' => [ - 'pattern' => '(account)/changeName', - 'load' => $dialogs['user.changeName']['load'], - 'submit' => $dialogs['user.changeName']['submit'], - ], + // change name + 'account.changeName' => [ + 'pattern' => '(account)/changeName', + 'load' => $dialogs['user.changeName']['load'], + 'submit' => $dialogs['user.changeName']['submit'], + ], - // change password - 'account.changePassword' => [ - 'pattern' => '(account)/changePassword', - 'load' => $dialogs['user.changePassword']['load'], - 'submit' => $dialogs['user.changePassword']['submit'], - ], + // change password + 'account.changePassword' => [ + 'pattern' => '(account)/changePassword', + 'load' => $dialogs['user.changePassword']['load'], + 'submit' => $dialogs['user.changePassword']['submit'], + ], - // change role - 'account.changeRole' => [ - 'pattern' => '(account)/changeRole', - 'load' => $dialogs['user.changeRole']['load'], - 'submit' => $dialogs['user.changeRole']['submit'], - ], + // change role + 'account.changeRole' => [ + 'pattern' => '(account)/changeRole', + 'load' => $dialogs['user.changeRole']['load'], + 'submit' => $dialogs['user.changeRole']['submit'], + ], - // delete - 'account.delete' => [ - 'pattern' => '(account)/delete', - 'load' => $dialogs['user.delete']['load'], - 'submit' => $dialogs['user.delete']['submit'], - ], + // delete + 'account.delete' => [ + 'pattern' => '(account)/delete', + 'load' => $dialogs['user.delete']['load'], + 'submit' => $dialogs['user.delete']['submit'], + ], - // change file name - 'account.file.changeName' => [ - 'pattern' => '(account)/files/(:any)/changeName', - 'load' => $dialogs['user.file.changeName']['load'], - 'submit' => $dialogs['user.file.changeName']['submit'], - ], + // change file name + 'account.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' => [ - 'pattern' => '(account)/files/(:any)/changeSort', - 'load' => $dialogs['user.file.changeSort']['load'], - 'submit' => $dialogs['user.file.changeSort']['submit'], - ], + // change file sort + 'account.file.changeSort' => [ + 'pattern' => '(account)/files/(:any)/changeSort', + 'load' => $dialogs['user.file.changeSort']['load'], + 'submit' => $dialogs['user.file.changeSort']['submit'], + ], - // delete - 'account.file.delete' => [ - 'pattern' => '(account)/files/(:any)/delete', - 'load' => $dialogs['user.file.delete']['load'], - 'submit' => $dialogs['user.file.delete']['submit'], - ], + // delete + 'account.file.delete' => [ + 'pattern' => '(account)/files/(:any)/delete', + 'load' => $dialogs['user.file.delete']['load'], + 'submit' => $dialogs['user.file.delete']['submit'], + ], ]; diff --git a/kirby/config/areas/account/dropdowns.php b/kirby/config/areas/account/dropdowns.php index 9cf2bd2..d739971 100644 --- a/kirby/config/areas/account/dropdowns.php +++ b/kirby/config/areas/account/dropdowns.php @@ -3,12 +3,12 @@ $dropdowns = require __DIR__ . '/../users/dropdowns.php'; return [ - 'account' => [ - 'pattern' => '(account)', - 'options' => $dropdowns['user']['options'] - ], - 'account.file' => [ - 'pattern' => '(account)/files/(:any)', - 'options' => $dropdowns['user.file']['options'] - ], + 'account' => [ + 'pattern' => '(account)', + 'options' => $dropdowns['user']['options'] + ], + 'account.file' => [ + 'pattern' => '(account)/files/(:any)', + 'options' => $dropdowns['user.file']['options'] + ], ]; diff --git a/kirby/config/areas/account/views.php b/kirby/config/areas/account/views.php index 98818ba..264edf9 100644 --- a/kirby/config/areas/account/views.php +++ b/kirby/config/areas/account/views.php @@ -1,34 +1,24 @@ [ - 'pattern' => 'account', - 'action' => fn () => [ - 'component' => 'k-account-view', - 'props' => kirby()->user()->panel()->props(), - ], - ], - 'account.file' => [ - 'pattern' => 'account/files/(:any)', - 'action' => function (string $filename) { - return Find::file('account', $filename)->panel()->view(); - } - ], - 'account.logout' => [ - 'pattern' => 'logout', - 'auth' => false, - 'action' => function () { - if ($user = kirby()->user()) { - $user->logout(); - } - Panel::go('login'); - }, - ], - 'account.password' => [ - 'pattern' => 'reset-password', - 'action' => fn () => ['component' => 'k-reset-password-view'] - ] + 'account' => [ + 'pattern' => 'account', + 'action' => fn () => [ + 'component' => 'k-account-view', + 'props' => App::instance()->user()->panel()->props(), + ], + ], + 'account.file' => [ + 'pattern' => 'account/files/(:any)', + 'action' => function (string $filename) { + return Find::file('account', $filename)->panel()->view(); + } + ], + 'account.password' => [ + 'pattern' => 'reset-password', + 'action' => fn () => ['component' => 'k-reset-password-view'] + ] ]; diff --git a/kirby/config/areas/files/dialogs.php b/kirby/config/areas/files/dialogs.php index 4c51ef0..44625eb 100644 --- a/kirby/config/areas/files/dialogs.php +++ b/kirby/config/areas/files/dialogs.php @@ -4,6 +4,7 @@ use Kirby\Cms\Find; use Kirby\Panel\Field; use Kirby\Panel\Panel; use Kirby\Toolkit\Escape; +use Kirby\Toolkit\I18n; /** * Shared file dialogs @@ -13,119 +14,119 @@ use Kirby\Toolkit\Escape; * the appropriate routes in the areas. */ return [ - 'changeName' => [ - 'load' => function (string $path, string $filename) { - $file = Find::file($path, $filename); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'name' => [ - 'label' => t('name'), - 'type' => 'slug', - 'required' => true, - 'icon' => 'title', - 'allow' => '@._-', - 'after' => '.' . $file->extension(), - 'preselect' => true - ] - ], - 'submitButton' => t('rename'), - 'value' => [ - 'name' => $file->name(), - ] - ] - ]; - }, - 'submit' => function (string $path, string $filename) { - $file = Find::file($path, $filename); - $renamed = $file->changeName(get('name')); - $oldUrl = $file->panel()->url(true); - $newUrl = $renamed->panel()->url(true); - $response = [ - 'event' => 'file.changeName', - 'dispatch' => [ - 'content/move' => [ - $oldUrl, - $newUrl - ] - ], - ]; + 'changeName' => [ + 'load' => function (string $path, string $filename) { + $file = Find::file($path, $filename); + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'name' => [ + 'label' => I18n::translate('name'), + 'type' => 'slug', + 'required' => true, + 'icon' => 'title', + 'allow' => '@._-', + 'after' => '.' . $file->extension(), + 'preselect' => true + ] + ], + 'submitButton' => I18n::translate('rename'), + 'value' => [ + 'name' => $file->name(), + ] + ] + ]; + }, + 'submit' => function (string $path, string $filename) { + $file = Find::file($path, $filename); + $renamed = $file->changeName($file->kirby()->request()->get('name')); + $oldUrl = $file->panel()->url(true); + $newUrl = $renamed->panel()->url(true); + $response = [ + 'event' => 'file.changeName', + 'dispatch' => [ + 'content/move' => [ + $oldUrl, + $newUrl + ] + ], + ]; - // check for a necessary redirect after the filename has changed - if (Panel::referrer() === $oldUrl && $oldUrl !== $newUrl) { - $response['redirect'] = $newUrl; - } + // check for a necessary redirect after the filename has changed + if (Panel::referrer() === $oldUrl && $oldUrl !== $newUrl) { + $response['redirect'] = $newUrl; + } - return $response; - } - ], + return $response; + } + ], - 'changeSort' => [ - 'load' => function (string $path, string $filename) { - $file = Find::file($path, $filename); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'position' => Field::filePosition($file) - ], - 'submitButton' => t('change'), - 'value' => [ - 'position' => $file->sort()->isEmpty() ? $file->siblings(false)->count() + 1 : $file->sort()->toInt(), - ] - ] - ]; - }, - 'submit' => function (string $path, string $filename) { - $file = Find::file($path, $filename); - $files = $file->siblings()->sorted(); - $ids = $files->keys(); - $newIndex = (int)(get('position')) - 1; - $oldIndex = $files->indexOf($file); + 'changeSort' => [ + 'load' => function (string $path, string $filename) { + $file = Find::file($path, $filename); + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'position' => Field::filePosition($file) + ], + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'position' => $file->sort()->isEmpty() ? $file->siblings(false)->count() + 1 : $file->sort()->toInt(), + ] + ] + ]; + }, + 'submit' => function (string $path, string $filename) { + $file = Find::file($path, $filename); + $files = $file->siblings()->sorted(); + $ids = $files->keys(); + $newIndex = (int)($file->kirby()->request()->get('position')) - 1; + $oldIndex = $files->indexOf($file); - array_splice($ids, $oldIndex, 1); - array_splice($ids, $newIndex, 0, $file->id()); + array_splice($ids, $oldIndex, 1); + array_splice($ids, $newIndex, 0, $file->id()); - $files->changeSort($ids); + $files->changeSort($ids); - return [ - 'event' => 'file.sort', - ]; - } - ], + return [ + 'event' => 'file.sort', + ]; + } + ], - 'delete' => [ - 'load' => function (string $path, string $filename) { - $file = Find::file($path, $filename); - return [ - 'component' => 'k-remove-dialog', - 'props' => [ - 'text' => tt('file.delete.confirm', [ - 'filename' => Escape::html($file->filename()) - ]), - ] - ]; - }, - 'submit' => function (string $path, string $filename) { - $file = Find::file($path, $filename); - $redirect = false; - $referrer = Panel::referrer(); - $url = $file->panel()->url(true); + 'delete' => [ + 'load' => function (string $path, string $filename) { + $file = Find::file($path, $filename); + return [ + 'component' => 'k-remove-dialog', + 'props' => [ + 'text' => I18n::template('file.delete.confirm', [ + 'filename' => Escape::html($file->filename()) + ]), + ] + ]; + }, + 'submit' => function (string $path, string $filename) { + $file = Find::file($path, $filename); + $redirect = false; + $referrer = Panel::referrer(); + $url = $file->panel()->url(true); - $file->delete(); + $file->delete(); - // redirect to the parent model URL - // if the dialog has been opened in the file view - if ($referrer === $url) { - $redirect = $file->parent()->panel()->url(true); - } + // redirect to the parent model URL + // if the dialog has been opened in the file view + if ($referrer === $url) { + $redirect = $file->parent()->panel()->url(true); + } - return [ - 'event' => 'file.delete', - 'dispatch' => ['content/remove' => [$url]], - 'redirect' => $redirect - ]; - } - ], + return [ + 'event' => 'file.delete', + 'dispatch' => ['content/remove' => [$url]], + 'redirect' => $redirect + ]; + } + ], ]; diff --git a/kirby/config/areas/files/dropdowns.php b/kirby/config/areas/files/dropdowns.php index d038294..8687a54 100644 --- a/kirby/config/areas/files/dropdowns.php +++ b/kirby/config/areas/files/dropdowns.php @@ -3,7 +3,7 @@ use Kirby\Cms\Find; return [ - 'file' => function (string $parent, string $filename) { - return Find::file($parent, $filename)->panel()->dropdown(); - } + 'file' => function (string $parent, string $filename) { + return Find::file($parent, $filename)->panel()->dropdown(); + } ]; diff --git a/kirby/config/areas/installation.php b/kirby/config/areas/installation.php index 9568e36..7f707cb 100644 --- a/kirby/config/areas/installation.php +++ b/kirby/config/areas/installation.php @@ -1,39 +1,40 @@ 'settings', - 'label' => t('view.installation'), - 'views' => [ - 'installation' => [ - 'pattern' => 'installation', - 'auth' => false, - 'action' => function () use ($kirby) { - $system = $kirby->system(); - return [ - 'component' => 'k-installation-view', - 'props' => [ - 'isInstallable' => $system->isInstallable(), - 'isInstalled' => $system->isInstalled(), - 'isOk' => $system->isOk(), - 'requirements' => $system->status(), - 'translations' => $kirby->translations()->values(function ($translation) { - return [ - 'text' => $translation->name(), - 'value' => $translation->code(), - ]; - }), - ] - ]; - } - ], - 'installation.fallback' => [ - 'pattern' => '(:all)', - 'auth' => false, - 'action' => fn () => Panel::go('installation') - ] - ] - ]; + return [ + 'icon' => 'settings', + 'label' => I18n::translate('view.installation'), + 'views' => [ + 'installation' => [ + 'pattern' => 'installation', + 'auth' => false, + 'action' => function () use ($kirby) { + $system = $kirby->system(); + return [ + 'component' => 'k-installation-view', + 'props' => [ + 'isInstallable' => $system->isInstallable(), + 'isInstalled' => $system->isInstalled(), + 'isOk' => $system->isOk(), + 'requirements' => $system->status(), + 'translations' => $kirby->translations()->values(function ($translation) { + return [ + 'text' => $translation->name(), + 'value' => $translation->code(), + ]; + }), + ] + ]; + } + ], + 'installation.fallback' => [ + 'pattern' => '(:all)', + 'auth' => false, + 'action' => fn () => Panel::go('installation') + ] + ] + ]; }; diff --git a/kirby/config/areas/languages.php b/kirby/config/areas/languages.php index ce0be15..d464c58 100644 --- a/kirby/config/areas/languages.php +++ b/kirby/config/areas/languages.php @@ -1,11 +1,13 @@ 'globe', - 'label' => t('view.languages'), - 'menu' => true, - 'dialogs' => require __DIR__ . '/languages/dialogs.php', - 'views' => require __DIR__ . '/languages/views.php' - ]; + return [ + 'icon' => 'globe', + 'label' => I18n::translate('view.languages'), + 'menu' => true, + 'dialogs' => require __DIR__ . '/languages/dialogs.php', + 'views' => require __DIR__ . '/languages/views.php' + ]; }; diff --git a/kirby/config/areas/languages/dialogs.php b/kirby/config/areas/languages/dialogs.php index d4bd5ed..a38b148 100644 --- a/kirby/config/areas/languages/dialogs.php +++ b/kirby/config/areas/languages/dialogs.php @@ -1,149 +1,155 @@ [ - 'label' => t('language.name'), - 'type' => 'text', - 'required' => true, - 'icon' => 'title' - ], - 'code' => [ - 'label' => t('language.code'), - 'type' => 'text', - 'required' => true, - 'counter' => false, - 'icon' => 'globe', - 'width' => '1/2' - ], - 'direction' => [ - 'label' => t('language.direction'), - 'type' => 'select', - 'required' => true, - 'empty' => false, - 'options' => [ - ['value' => 'ltr', 'text' => t('language.direction.ltr')], - ['value' => 'rtl', 'text' => t('language.direction.rtl')] - ], - 'width' => '1/2' - ], - 'locale' => [ - 'label' => t('language.locale'), - 'type' => 'text', - ], + 'name' => [ + 'label' => I18n::translate('language.name'), + 'type' => 'text', + 'required' => true, + 'icon' => 'title' + ], + 'code' => [ + 'label' => I18n::translate('language.code'), + 'type' => 'text', + 'required' => true, + 'counter' => false, + 'icon' => 'globe', + 'width' => '1/2' + ], + 'direction' => [ + 'label' => I18n::translate('language.direction'), + 'type' => 'select', + 'required' => true, + 'empty' => false, + 'options' => [ + ['value' => 'ltr', 'text' => I18n::translate('language.direction.ltr')], + ['value' => 'rtl', 'text' => I18n::translate('language.direction.rtl')] + ], + 'width' => '1/2' + ], + 'locale' => [ + 'label' => I18n::translate('language.locale'), + 'type' => 'text', + ], ]; return [ - // create language - 'language.create' => [ - 'pattern' => 'languages/create', - 'load' => function () use ($languageDialogFields) { - return [ - 'component' => 'k-language-dialog', - 'props' => [ - 'fields' => $languageDialogFields, - 'submitButton' => t('language.create'), - 'value' => [ - 'code' => '', - 'direction' => 'ltr', - 'locale' => '', - 'name' => '', - ] - ] - ]; - }, - 'submit' => function () { - kirby()->languages()->create([ - 'code' => get('code'), - 'direction' => get('direction'), - 'locale' => get('locale'), - 'name' => get('name'), - ]); - return [ - 'event' => 'language.create' - ]; - } - ], + // create language + 'language.create' => [ + 'pattern' => 'languages/create', + 'load' => function () use ($languageDialogFields) { + return [ + 'component' => 'k-language-dialog', + 'props' => [ + 'fields' => $languageDialogFields, + 'submitButton' => I18n::translate('language.create'), + 'value' => [ + 'code' => '', + 'direction' => 'ltr', + 'locale' => '', + 'name' => '', + ] + ] + ]; + }, + 'submit' => function () { + $kirby = App::instance(); - // delete language - 'language.delete' => [ - 'pattern' => 'languages/(:any)/delete', - 'load' => function (string $id) { - $language = Find::language($id); - return [ - 'component' => 'k-remove-dialog', - 'props' => [ - 'text' => tt('language.delete.confirm', [ - 'name' => Escape::html($language->name()) - ]) - ] - ]; - }, - 'submit' => function (string $id) { - Find::language($id)->delete(); - return [ - 'event' => 'language.delete', - ]; - } - ], + $data = $kirby->request()->get([ + 'code', + 'direction', + 'locale', + 'name' + ]); + $kirby->languages()->create($data); - // update language - 'language.update' => [ - 'pattern' => 'languages/(:any)/update', - 'load' => function (string $id) use ($languageDialogFields) { - $language = Find::language($id); - $fields = $languageDialogFields; - $locale = $language->locale(); + return [ + 'event' => 'language.create' + ]; + } + ], - // use the first locale key if there's only one - if (count($locale) === 1) { - $locale = A::first($locale); - } + // delete language + 'language.delete' => [ + 'pattern' => 'languages/(:any)/delete', + 'load' => function (string $id) { + $language = Find::language($id); + return [ + 'component' => 'k-remove-dialog', + 'props' => [ + 'text' => I18n::template('language.delete.confirm', [ + 'name' => Escape::html($language->name()) + ]) + ] + ]; + }, + 'submit' => function (string $id) { + Find::language($id)->delete(); + return [ + 'event' => 'language.delete', + ]; + } + ], - // the code of an existing language cannot be changed - $fields['code']['disabled'] = true; + // update language + 'language.update' => [ + 'pattern' => 'languages/(:any)/update', + 'load' => function (string $id) use ($languageDialogFields) { + $language = Find::language($id); + $fields = $languageDialogFields; + $locale = $language->locale(); - // if the locale settings is more complex than just a - // single string, the text field won't do it anymore. - // Changes can only be made in the language file and - // we display a warning box instead. - if (is_array($locale) === true) { - $fields['locale'] = [ - 'label' => $fields['locale']['label'], - 'type' => 'info', - 'text' => t('language.locale.warning') - ]; - } + // use the first locale key if there's only one + if (count($locale) === 1) { + $locale = A::first($locale); + } - return [ - 'component' => 'k-language-dialog', - 'props' => [ - 'fields' => $fields, - 'submitButton' => t('save'), - 'value' => [ - 'code' => $language->code(), - 'direction' => $language->direction(), - 'locale' => $locale, - 'name' => $language->name(), - 'rules' => $language->rules(), - ] - ] - ]; - }, - 'submit' => function (string $id) { - $language = Find::language($id)->update([ - 'direction' => get('direction'), - 'locale' => get('locale'), - 'name' => get('name'), - ]); - return [ - 'event' => 'language.update' - ]; - } - ], + // the code of an existing language cannot be changed + $fields['code']['disabled'] = true; + + // if the locale settings is more complex than just a + // single string, the text field won't do it anymore. + // Changes can only be made in the language file and + // we display a warning box instead. + if (is_array($locale) === true) { + $fields['locale'] = [ + 'label' => $fields['locale']['label'], + 'type' => 'info', + 'text' => I18n::translate('language.locale.warning') + ]; + } + + return [ + 'component' => 'k-language-dialog', + 'props' => [ + 'fields' => $fields, + 'submitButton' => I18n::translate('save'), + 'value' => [ + 'code' => $language->code(), + 'direction' => $language->direction(), + 'locale' => $locale, + 'name' => $language->name(), + 'rules' => $language->rules(), + ] + ] + ]; + }, + 'submit' => function (string $id) { + $kirby = App::instance(); + + $data = $kirby->request()->get(['direction', 'locale', 'name']); + $language = Find::language($id)->update($data); + + return [ + 'event' => 'language.update' + ]; + } + ], ]; diff --git a/kirby/config/areas/languages/views.php b/kirby/config/areas/languages/views.php index f5bf842..0eee9da 100644 --- a/kirby/config/areas/languages/views.php +++ b/kirby/config/areas/languages/views.php @@ -1,24 +1,25 @@ [ - 'pattern' => 'languages', - 'action' => function () { - $kirby = kirby(); + 'languages' => [ + 'pattern' => 'languages', + 'action' => function () { + $kirby = App::instance(); - return [ - 'component' => 'k-languages-view', - 'props' => [ - 'languages' => $kirby->languages()->values(fn ($language) => [ - 'default' => $language->isDefault(), - 'id' => $language->code(), - 'info' => Escape::html($language->code()), - 'text' => Escape::html($language->name()), - ]) - ] - ]; - } - ], + return [ + 'component' => 'k-languages-view', + 'props' => [ + 'languages' => $kirby->languages()->values(fn ($language) => [ + 'default' => $language->isDefault(), + 'id' => $language->code(), + 'info' => Escape::html($language->code()), + 'text' => Escape::html($language->name()), + ]) + ] + ]; + } + ], ]; diff --git a/kirby/config/areas/login.php b/kirby/config/areas/login.php index d323fda..56f30cb 100644 --- a/kirby/config/areas/login.php +++ b/kirby/config/areas/login.php @@ -1,43 +1,44 @@ 'user', - 'label' => t('login'), - 'views' => [ - 'login' => [ - 'pattern' => 'login', - 'auth' => false, - 'action' => function () use ($kirby) { - $system = $kirby->system(); - $status = $kirby->auth()->status(); - return [ - 'component' => 'k-login-view', - 'props' => [ - 'methods' => array_keys($system->loginMethods()), - 'pending' => [ - 'email' => $status->email(), - 'challenge' => $status->challenge() - ] - ], - ]; - } - ], - 'login.fallback' => [ - 'pattern' => '(:all)', - 'auth' => false, - 'action' => function ($path) use ($kirby) { - /** - * Store the current path in the session - * Once the user is logged in, the path will - * be used to redirect to that view again - */ - $kirby->session()->set('panel.path', $path); - Panel::go('login'); - } - ] - ] - ]; + return [ + 'icon' => 'user', + 'label' => I18n::translate('login'), + 'views' => [ + 'login' => [ + 'pattern' => 'login', + 'auth' => false, + 'action' => function () use ($kirby) { + $system = $kirby->system(); + $status = $kirby->auth()->status(); + return [ + 'component' => 'k-login-view', + 'props' => [ + 'methods' => array_keys($system->loginMethods()), + 'pending' => [ + 'email' => $status->email(), + 'challenge' => $status->challenge() + ] + ], + ]; + } + ], + 'login.fallback' => [ + 'pattern' => '(:all)', + 'auth' => false, + 'action' => function ($path) use ($kirby) { + /** + * Store the current path in the session + * Once the user is logged in, the path will + * be used to redirect to that view again + */ + $kirby->session()->set('panel.path', $path); + Panel::go('login'); + } + ] + ] + ]; }; diff --git a/kirby/config/areas/logout.php b/kirby/config/areas/logout.php new file mode 100644 index 0000000..5dc4a50 --- /dev/null +++ b/kirby/config/areas/logout.php @@ -0,0 +1,21 @@ + 'user', + 'label' => I18n::translate('logout'), + 'views' => [ + 'logout' => [ + 'pattern' => 'logout', + 'auth' => false, + 'action' => function () use ($kirby) { + $kirby->auth()->logout(); + Panel::go('login'); + }, + ] + ] + ]; +}; diff --git a/kirby/config/areas/site.php b/kirby/config/areas/site.php index 0e04445..3f082be 100644 --- a/kirby/config/areas/site.php +++ b/kirby/config/areas/site.php @@ -1,17 +1,18 @@ function () use ($kirby) { - return $kirby->site()->title()->or(t('view.site'))->toString(); - }, - 'icon' => 'home', - 'label' => $kirby->site()->blueprint()->title() ?? t('view.site'), - 'menu' => true, - 'dialogs' => require __DIR__ . '/site/dialogs.php', - 'dropdowns' => require __DIR__ . '/site/dropdowns.php', - 'searches' => require __DIR__ . '/site/searches.php', - 'views' => require __DIR__ . '/site/views.php', - ]; + return [ + 'breadcrumbLabel' => function () use ($kirby) { + return $kirby->site()->title()->or(I18n::translate('view.site'))->toString(); + }, + 'icon' => 'home', + 'label' => $kirby->site()->blueprint()->title() ?? I18n::translate('view.site'), + 'menu' => true, + 'dialogs' => require __DIR__ . '/site/dialogs.php', + 'dropdowns' => require __DIR__ . '/site/dropdowns.php', + 'searches' => require __DIR__ . '/site/searches.php', + 'views' => require __DIR__ . '/site/views.php', + ]; }; diff --git a/kirby/config/areas/site/dialogs.php b/kirby/config/areas/site/dialogs.php index f8123d0..3101464 100644 --- a/kirby/config/areas/site/dialogs.php +++ b/kirby/config/areas/site/dialogs.php @@ -1,551 +1,584 @@ [ - 'pattern' => 'pages/(:any)/changeSort', - 'load' => function (string $id) { - $page = Find::page($id); - $position = null; + // change page position + 'page.changeSort' => [ + 'pattern' => 'pages/(:any)/changeSort', + 'load' => function (string $id) { + $page = Find::page($id); - if ($page->blueprint()->num() !== 'default') { - throw new PermissionException([ - 'key' => 'page.sort.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + if ($page->blueprint()->num() !== 'default') { + throw new PermissionException([ + 'key' => 'page.sort.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'position' => Field::pagePosition($page), - ], - 'submitButton' => t('change'), - 'value' => [ - 'position' => $page->panel()->position() - ] - ] - ]; - }, - 'submit' => function (string $id) { - Find::page($id)->changeStatus('listed', get('position')); - return [ - 'event' => 'page.sort', - ]; - } - ], + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'position' => Field::pagePosition($page), + ], + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'position' => $page->panel()->position() + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - // change page status - 'page.changeStatus' => [ - 'pattern' => 'pages/(:any)/changeStatus', - 'load' => function (string $id) { - $page = Find::page($id); - $blueprint = $page->blueprint(); - $status = $page->status(); - $states = []; - $position = null; + Find::page($id)->changeStatus( + 'listed', + $request->get('position') + ); - foreach ($blueprint->status() as $key => $state) { - $states[] = [ - 'value' => $key, - 'text' => $state['label'], - 'info' => $state['text'], - ]; - } + return [ + 'event' => 'page.sort', + ]; + } + ], - if ($status === 'draft') { - $errors = $page->errors(); + // change page status + 'page.changeStatus' => [ + 'pattern' => 'pages/(:any)/changeStatus', + 'load' => function (string $id) { + $page = Find::page($id); + $blueprint = $page->blueprint(); + $status = $page->status(); + $states = []; + $position = null; - // switch to the error dialog if there are - // errors and the draft cannot be published - if (count($errors) > 0) { - return [ - 'component' => 'k-error-dialog', - 'props' => [ - 'message' => t('error.page.changeStatus.incomplete'), - 'details' => $errors, - ] - ]; - } - } + foreach ($blueprint->status() as $key => $state) { + $states[] = [ + 'value' => $key, + 'text' => $state['label'], + 'info' => $state['text'], + ]; + } - $fields = [ - 'status' => [ - 'label' => t('page.changeStatus.select'), - 'type' => 'radio', - 'required' => true, - 'options' => $states - ] - ]; + if ($status === 'draft') { + $errors = $page->errors(); - if ($blueprint->num() === 'default') { - $fields['position'] = Field::pagePosition($page, [ - 'when' => [ - 'status' => 'listed' - ] - ]); + // switch to the error dialog if there are + // errors and the draft cannot be published + if (count($errors) > 0) { + return [ + 'component' => 'k-error-dialog', + 'props' => [ + 'message' => I18n::translate('error.page.changeStatus.incomplete'), + 'details' => $errors, + ] + ]; + } + } - $position = $page->panel()->position(); - } + $fields = [ + 'status' => [ + 'label' => I18n::translate('page.changeStatus.select'), + 'type' => 'radio', + 'required' => true, + 'options' => $states + ] + ]; - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => $fields, - 'submitButton' => t('change'), - 'value' => [ - 'status' => $status, - 'position' => $position - ] - ] - ]; - }, - 'submit' => function (string $id) { - Find::page($id)->changeStatus(get('status'), get('position')); - return [ - 'event' => 'page.changeStatus', - ]; - } - ], + if ($blueprint->num() === 'default') { + $fields['position'] = Field::pagePosition($page, [ + 'when' => [ + 'status' => 'listed' + ] + ]); - // change template - 'page.changeTemplate' => [ - 'pattern' => 'pages/(:any)/changeTemplate', - 'load' => function (string $id) { - $page = Find::page($id); - $blueprints = $page->blueprints(); + $position = $page->panel()->position(); + } - if (count($blueprints) <= 1) { - throw new Exception([ - 'key' => 'page.changeTemplate.invalid', - 'data' => [ - 'slug' => $id - ] - ]); - } + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => $fields, + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'status' => $status, + 'position' => $position + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'template' => Field::template($blueprints, [ - 'required' => true - ]) - ], - 'submitButton' => t('change'), - 'value' => [ - 'template' => $page->intendedTemplate()->name() - ] - ] - ]; - }, - 'submit' => function (string $id) { - Find::page($id)->changeTemplate(get('template')); - return [ - 'event' => 'page.changeTemplate', - ]; - } - ], + Find::page($id)->changeStatus( + $request->get('status'), + $request->get('position') + ); - // change title - 'page.changeTitle' => [ - 'pattern' => 'pages/(:any)/changeTitle', - 'load' => function (string $id) { - $page = Find::page($id); - $permissions = $page->permissions(); - $select = get('select', 'title'); + return [ + 'event' => 'page.changeStatus', + ]; + } + ], - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'title' => Field::title([ - 'required' => true, - 'preselect' => $select === 'title', - 'disabled' => $permissions->can('changeTitle') === false - ]), - 'slug' => Field::slug([ - 'required' => true, - 'preselect' => $select === 'slug', - 'path' => $page->parent() ? '/' . $page->parent()->id() . '/' : '/', - 'disabled' => $permissions->can('changeSlug') === false, - 'wizard' => [ - 'text' => t('page.changeSlug.fromTitle'), - 'field' => 'title' - ] - ]) - ], - 'autofocus' => false, - 'submitButton' => t('change'), - 'value' => [ - 'title' => $page->title()->value(), - 'slug' => $page->slug(), - ] - ] - ]; - }, - 'submit' => function (string $id) { - $page = Find::page($id); - $title = trim(get('title', '')); - $slug = trim(get('slug', '')); + // change template + 'page.changeTemplate' => [ + 'pattern' => 'pages/(:any)/changeTemplate', + 'load' => function (string $id) { + $page = Find::page($id); + $blueprints = $page->blueprints(); - // basic input validation before we move on - if (Str::length($title) === 0) { - throw new InvalidArgumentException([ - 'key' => 'page.changeTitle.empty' - ]); - } + if (count($blueprints) <= 1) { + throw new Exception([ + 'key' => 'page.changeTemplate.invalid', + 'data' => [ + 'slug' => $id + ] + ]); + } - if (Str::length($slug) === 0) { - throw new InvalidArgumentException([ - 'key' => 'page.slug.invalid' - ]); - } + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'template' => Field::template($blueprints, [ + 'required' => true + ]) + ], + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'template' => $page->intendedTemplate()->name() + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - // nothing changed - if ($page->title()->value() === $title && $page->slug() === $slug) { - return true; - } + Find::page($id)->changeTemplate($request->get('template')); - // prepare the response - $response = [ - 'event' => [] - ]; + return [ + 'event' => 'page.changeTemplate', + ]; + } + ], - // the page title changed - if ($page->title()->value() !== $title) { - $page->changeTitle($title); - $response['event'][] = 'page.changeTitle'; - } + // change title + 'page.changeTitle' => [ + 'pattern' => 'pages/(:any)/changeTitle', + 'load' => function (string $id) { + $request = App::instance()->request(); - // the slug changed - if ($page->slug() !== $slug) { - $newPage = $page->changeSlug($slug); - $response['event'][] = 'page.changeSlug'; - $response['dispatch'] = [ - 'content/move' => [ - $oldUrl = $page->panel()->url(true), - $newUrl = $newPage->panel()->url(true) - ] - ]; + $page = Find::page($id); + $permissions = $page->permissions(); + $select = $request->get('select', 'title'); - // check for a necessary redirect after the slug has changed - if (Panel::referrer() === $oldUrl && $oldUrl !== $newUrl) { - $response['redirect'] = $newUrl; - } - } + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'title' => Field::title([ + 'required' => true, + 'preselect' => $select === 'title', + 'disabled' => $permissions->can('changeTitle') === false + ]), + 'slug' => Field::slug([ + 'required' => true, + 'preselect' => $select === 'slug', + 'path' => $page->parent() ? '/' . $page->parent()->id() . '/' : '/', + 'disabled' => $permissions->can('changeSlug') === false, + 'wizard' => [ + 'text' => I18n::translate('page.changeSlug.fromTitle'), + 'field' => 'title' + ] + ]) + ], + 'autofocus' => false, + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'title' => $page->title()->value(), + 'slug' => $page->slug(), + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - return $response; - } - ], + $page = Find::page($id); + $title = trim($request->get('title', '')); + $slug = trim($request->get('slug', '')); - // create a new page - 'page.create' => [ - 'pattern' => 'pages/create', - 'load' => function () { - // the parent model for the new page - $parent = get('parent', 'site'); + // basic input validation before we move on + if (Str::length($title) === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.changeTitle.empty' + ]); + } - // the view on which the add button is located - // this is important to find the right section - // and provide the correct templates for the new page - $view = get('view', $parent); + if (Str::length($slug) === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.slug.invalid' + ]); + } - // templates will be fetched depending on the - // section settings in the blueprint - $section = get('section'); + // nothing changed + if ($page->title()->value() === $title && $page->slug() === $slug) { + return true; + } - // this is the parent model - $model = Find::parent($parent); + // prepare the response + $response = [ + 'event' => [] + ]; - // this is the view model - // i.e. site if the add button is on - // the dashboard - $view = Find::parent($view); + // the page title changed + if ($page->title()->value() !== $title) { + $page->changeTitle($title); + $response['event'][] = 'page.changeTitle'; + } - // available blueprints/templates for the new page - // are always loaded depending on the matching section - // in the view model blueprint - $blueprints = $view->blueprints($section); + // the slug changed + if ($page->slug() !== $slug) { + $newPage = $page->changeSlug($slug); + $response['event'][] = 'page.changeSlug'; + $response['dispatch'] = [ + 'content/move' => [ + $oldUrl = $page->panel()->url(true), + $newUrl = $newPage->panel()->url(true) + ] + ]; - // the pre-selected template - $template = $blueprints[0]['name'] ?? $blueprints[0]['value'] ?? null; + // check for a necessary redirect after the slug has changed + if (Panel::referrer() === $oldUrl && $oldUrl !== $newUrl) { + $response['redirect'] = $newUrl; + } + } - $fields = [ - 'parent' => Field::hidden(), - 'title' => Field::title([ - 'required' => true, - 'preselect' => true - ]), - 'slug' => Field::slug([ - 'required' => true, - 'sync' => 'title', - 'path' => empty($model->id()) === false ? '/' . $model->id() . '/' : '/' - ]), - 'template' => Field::hidden() - ]; + return $response; + } + ], - // only show template field if > 1 templates available - // or when in debug mode - if (count($blueprints) > 1 || option('debug') === true) { - $fields['template'] = Field::template($blueprints, [ - 'required' => true - ]); - } + // create a new page + 'page.create' => [ + 'pattern' => 'pages/create', + 'load' => function () { + $kirby = App::instance(); + $request = $kirby->request(); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => $fields, - 'submitButton' => t('page.draft.create'), - 'value' => [ - 'parent' => $parent, - 'slug' => '', - 'template' => $template, - 'title' => '', - ] - ] - ]; - }, - 'submit' => function () { - $title = trim(get('title', '')); + // the parent model for the new page + $parent = $request->get('parent', 'site'); - if (Str::length($title) === 0) { - throw new InvalidArgumentException([ - 'key' => 'page.changeTitle.empty' - ]); - } + // the view on which the add button is located + // this is important to find the right section + // and provide the correct templates for the new page + $view = $request->get('view', $parent); - $page = Find::parent(get('parent', 'site'))->createChild([ - 'content' => ['title' => $title], - 'slug' => get('slug'), - 'template' => get('template'), - ]); + // templates will be fetched depending on the + // section settings in the blueprint + $section = $request->get('section'); - return [ - 'event' => 'page.create', - 'redirect' => $page->panel()->url(true) - ]; - } - ], + // this is the parent model + $model = Find::parent($parent); - // delete page - 'page.delete' => [ - 'pattern' => 'pages/(:any)/delete', - 'load' => function (string $id) { - $page = Find::page($id); - $text = tt('page.delete.confirm', [ - 'title' => Escape::html($page->title()->value()) - ]); + // this is the view model + // i.e. site if the add button is on + // the dashboard + $view = Find::parent($view); - if ($page->childrenAndDrafts()->count() > 0) { - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'info' => [ - 'type' => 'info', - 'theme' => 'negative', - 'text' => t('page.delete.confirm.subpages') - ], - 'check' => [ - 'label' => t('page.delete.confirm.title'), - 'type' => 'text', - 'counter' => false - ] - ], - 'size' => 'medium', - 'submitButton' => t('delete'), - 'text' => $text, - 'theme' => 'negative', - ] - ]; - } + // available blueprints/templates for the new page + // are always loaded depending on the matching section + // in the view model blueprint + $blueprints = $view->blueprints($section); - return [ - 'component' => 'k-remove-dialog', - 'props' => [ - 'text' => $text - ] - ]; - }, - 'submit' => function (string $id) { - $page = Find::page($id); - $redirect = false; - $referrer = Panel::referrer(); - $url = $page->panel()->url(true); + // the pre-selected template + $template = $blueprints[0]['name'] ?? $blueprints[0]['value'] ?? null; - if ($page->childrenAndDrafts()->count() > 0 && get('check') !== $page->title()->value()) { - throw new InvalidArgumentException(['key' => 'page.delete.confirm']); - } + $fields = [ + 'parent' => Field::hidden(), + 'title' => Field::title([ + 'required' => true, + 'preselect' => true + ]), + 'slug' => Field::slug([ + 'required' => true, + 'sync' => 'title', + 'path' => empty($model->id()) === false ? '/' . $model->id() . '/' : '/' + ]), + 'template' => Field::hidden() + ]; - $page->delete(true); + // only show template field if > 1 templates available + // or when in debug mode + if (count($blueprints) > 1 || $kirby->option('debug') === true) { + $fields['template'] = Field::template($blueprints, [ + 'required' => true + ]); + } - // redirect to the parent model URL - // if the dialog has been opened in the page view - if ($referrer === $url) { - $redirect = $page->parentModel()->panel()->url(true); - } + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => $fields, + 'submitButton' => I18n::translate('page.draft.create'), + 'value' => [ + 'parent' => $parent, + 'slug' => '', + 'template' => $template, + 'title' => '', + ] + ] + ]; + }, + 'submit' => function () { + $request = App::instance()->request(); + $title = trim($request->get('title', '')); - return [ - 'event' => 'page.delete', - 'dispatch' => ['content/remove' => [$url]], - 'redirect' => $redirect - ]; - } - ], + if (Str::length($title) === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.changeTitle.empty' + ]); + } - // duplicate page - 'page.duplicate' => [ - 'pattern' => 'pages/(:any)/duplicate', - 'load' => function (string $id) { - $page = Find::page($id); - $hasChildren = $page->hasChildren(); - $hasFiles = $page->hasFiles(); - $toggleWidth = '1/' . count(array_filter([$hasChildren, $hasFiles])); + $page = Find::parent($request->get('parent', 'site'))->createChild([ + 'content' => ['title' => $title], + 'slug' => $request->get('slug'), + 'template' => $request->get('template'), + ]); - $fields = [ - 'title' => Field::title([ - 'required' => true - ]), - 'slug' => Field::slug([ - 'required' => true, - 'path' => $page->parent() ? '/' . $page->parent()->id() . '/' : '/', - 'wizard' => [ - 'text' => t('page.changeSlug.fromTitle'), - 'field' => 'title' - ] - ]) - ]; + return [ + 'event' => 'page.create', + 'redirect' => $page->panel()->url(true) + ]; + } + ], - if ($hasFiles === true) { - $fields['files'] = [ - 'label' => t('page.duplicate.files'), - 'type' => 'toggle', - 'required' => true, - 'width' => $toggleWidth - ]; - } + // delete page + 'page.delete' => [ + 'pattern' => 'pages/(:any)/delete', + 'load' => function (string $id) { + $page = Find::page($id); + $text = I18n::template('page.delete.confirm', [ + 'title' => Escape::html($page->title()->value()) + ]); - if ($hasChildren === true) { - $fields['children'] = [ - 'label' => t('page.duplicate.pages'), - 'type' => 'toggle', - 'required' => true, - 'width' => $toggleWidth - ]; - } + if ($page->childrenAndDrafts()->count() > 0) { + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'info' => [ + 'type' => 'info', + 'theme' => 'negative', + 'text' => I18n::translate('page.delete.confirm.subpages') + ], + 'check' => [ + 'label' => I18n::translate('page.delete.confirm.title'), + 'type' => 'text', + 'counter' => false + ] + ], + 'size' => 'medium', + 'submitButton' => I18n::translate('delete'), + 'text' => $text, + 'theme' => 'negative', + ] + ]; + } - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => $fields, - 'submitButton' => t('duplicate'), - 'value' => [ - 'children' => false, - 'files' => false, - 'slug' => $page->slug() . '-' . Str::slug(t('page.duplicate.appendix')), - 'title' => $page->title() . ' ' . t('page.duplicate.appendix') - ] - ] - ]; - }, - 'submit' => function (string $id) { - $newPage = Find::page($id)->duplicate(get('slug'), [ - 'children' => (bool)get('children'), - 'files' => (bool)get('files'), - 'title' => (string)get('title'), - ]); + return [ + 'component' => 'k-remove-dialog', + 'props' => [ + 'text' => $text + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - return [ - 'event' => 'page.duplicate', - 'redirect' => $newPage->panel()->url(true) - ]; - } - ], + $page = Find::page($id); + $redirect = false; + $referrer = Panel::referrer(); + $url = $page->panel()->url(true); - // change filename - 'page.file.changeName' => [ - 'pattern' => '(pages/.*?)/files/(:any)/changeName', - 'load' => $files['changeName']['load'], - 'submit' => $files['changeName']['submit'], - ], + if ( + $page->childrenAndDrafts()->count() > 0 && + $request->get('check') !== $page->title()->value() + ) { + throw new InvalidArgumentException(['key' => 'page.delete.confirm']); + } - // change sort - 'page.file.changeSort' => [ - 'pattern' => '(pages/.*?)/files/(:any)/changeSort', - 'load' => $files['changeSort']['load'], - 'submit' => $files['changeSort']['submit'], - ], + $page->delete(true); - // delete - 'page.file.delete' => [ - 'pattern' => '(pages/.*?)/files/(:any)/delete', - 'load' => $files['delete']['load'], - 'submit' => $files['delete']['submit'], - ], + // redirect to the parent model URL + // if the dialog has been opened in the page view + if ($referrer === $url) { + $redirect = $page->parentModel()->panel()->url(true); + } - // change site title - 'site.changeTitle' => [ - 'pattern' => 'site/changeTitle', - 'load' => function () { - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'title' => Field::title([ - 'required' => true, - 'preselect' => true - ]) - ], - 'submitButton' => t('rename'), - 'value' => [ - 'title' => site()->title()->value() - ] - ] - ]; - }, - 'submit' => function () { - site()->changeTitle(get('title')); - return [ - 'event' => 'site.changeTitle', - ]; - } - ], + return [ + 'event' => 'page.delete', + 'dispatch' => ['content/remove' => [$url]], + 'redirect' => $redirect + ]; + } + ], - // change filename - 'site.file.changeName' => [ - 'pattern' => '(site)/files/(:any)/changeName', - 'load' => $files['changeName']['load'], - 'submit' => $files['changeName']['submit'], - ], + // duplicate page + 'page.duplicate' => [ + 'pattern' => 'pages/(:any)/duplicate', + 'load' => function (string $id) { + $page = Find::page($id); + $hasChildren = $page->hasChildren(); + $hasFiles = $page->hasFiles(); + $toggleWidth = '1/' . count(array_filter([$hasChildren, $hasFiles])); - // change sort - 'site.file.changeSort' => [ - 'pattern' => '(site)/files/(:any)/changeSort', - 'load' => $files['changeSort']['load'], - 'submit' => $files['changeSort']['submit'], - ], + $fields = [ + 'title' => Field::title([ + 'required' => true + ]), + 'slug' => Field::slug([ + 'required' => true, + 'path' => $page->parent() ? '/' . $page->parent()->id() . '/' : '/', + 'wizard' => [ + 'text' => I18n::translate('page.changeSlug.fromTitle'), + 'field' => 'title' + ] + ]) + ]; - // delete - 'site.file.delete' => [ - 'pattern' => '(site)/files/(:any)/delete', - 'load' => $files['delete']['load'], - 'submit' => $files['delete']['submit'], - ], + if ($hasFiles === true) { + $fields['files'] = [ + 'label' => I18n::translate('page.duplicate.files'), + 'type' => 'toggle', + 'required' => true, + 'width' => $toggleWidth + ]; + } + + if ($hasChildren === true) { + $fields['children'] = [ + 'label' => I18n::translate('page.duplicate.pages'), + 'type' => 'toggle', + 'required' => true, + 'width' => $toggleWidth + ]; + } + + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => $fields, + 'submitButton' => I18n::translate('duplicate'), + 'value' => [ + 'children' => false, + 'files' => false, + 'slug' => $page->slug() . '-' . Str::slug(I18n::translate('page.duplicate.appendix')), + 'title' => $page->title() . ' ' . I18n::translate('page.duplicate.appendix') + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); + + $newPage = Find::page($id)->duplicate($request->get('slug'), [ + 'children' => (bool)$request->get('children'), + 'files' => (bool)$request->get('files'), + 'title' => (string)$request->get('title'), + ]); + + return [ + 'event' => 'page.duplicate', + 'redirect' => $newPage->panel()->url(true) + ]; + } + ], + + // change filename + 'page.file.changeName' => [ + 'pattern' => '(pages/.*?)/files/(:any)/changeName', + 'load' => $files['changeName']['load'], + 'submit' => $files['changeName']['submit'], + ], + + // change sort + 'page.file.changeSort' => [ + 'pattern' => '(pages/.*?)/files/(:any)/changeSort', + 'load' => $files['changeSort']['load'], + 'submit' => $files['changeSort']['submit'], + ], + + // delete + 'page.file.delete' => [ + 'pattern' => '(pages/.*?)/files/(:any)/delete', + 'load' => $files['delete']['load'], + 'submit' => $files['delete']['submit'], + ], + + // change site title + 'site.changeTitle' => [ + 'pattern' => 'site/changeTitle', + 'load' => function () { + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'title' => Field::title([ + 'required' => true, + 'preselect' => true + ]) + ], + 'submitButton' => I18n::translate('rename'), + 'value' => [ + 'title' => App::instance()->site()->title()->value() + ] + ] + ]; + }, + 'submit' => function () { + $kirby = App::instance(); + + $kirby->site()->changeTitle($kirby->request()->get('title')); + return [ + 'event' => 'site.changeTitle', + ]; + } + ], + + // change filename + 'site.file.changeName' => [ + 'pattern' => '(site)/files/(:any)/changeName', + 'load' => $files['changeName']['load'], + 'submit' => $files['changeName']['submit'], + ], + + // change sort + 'site.file.changeSort' => [ + 'pattern' => '(site)/files/(:any)/changeSort', + 'load' => $files['changeSort']['load'], + 'submit' => $files['changeSort']['submit'], + ], + + // delete + 'site.file.delete' => [ + 'pattern' => '(site)/files/(:any)/delete', + 'load' => $files['delete']['load'], + 'submit' => $files['delete']['submit'], + ], ]; diff --git a/kirby/config/areas/site/dropdowns.php b/kirby/config/areas/site/dropdowns.php index c498c00..c08853b 100644 --- a/kirby/config/areas/site/dropdowns.php +++ b/kirby/config/areas/site/dropdowns.php @@ -5,22 +5,22 @@ use Kirby\Panel\Dropdown; $files = require __DIR__ . '/../files/dropdowns.php'; return [ - 'changes' => [ - 'pattern' => 'changes', - 'options' => fn () => Dropdown::changes() - ], - 'page' => [ - 'pattern' => 'pages/(:any)', - 'options' => function (string $path) { - return Find::page($path)->panel()->dropdown(); - } - ], - 'page.file' => [ - 'pattern' => '(pages/.*?)/files/(:any)', - 'options' => $files['file'] - ], - 'site.file' => [ - 'pattern' => '(site)/files/(:any)', - 'options' => $files['file'] - ] + 'changes' => [ + 'pattern' => 'changes', + 'options' => fn () => Dropdown::changes() + ], + 'page' => [ + 'pattern' => 'pages/(:any)', + 'options' => function (string $path) { + return Find::page($path)->panel()->dropdown(); + } + ], + 'page.file' => [ + 'pattern' => '(pages/.*?)/files/(:any)', + 'options' => $files['file'] + ], + 'site.file' => [ + 'pattern' => '(site)/files/(:any)', + 'options' => $files['file'] + ] ]; diff --git a/kirby/config/areas/site/searches.php b/kirby/config/areas/site/searches.php index 14b5479..6719bb2 100644 --- a/kirby/config/areas/site/searches.php +++ b/kirby/config/areas/site/searches.php @@ -1,55 +1,57 @@ [ - 'label' => t('pages'), - 'icon' => 'page', - 'query' => function (string $query = null) { - $pages = site() - ->index(true) - ->search($query) - ->filter('isReadable', true) - ->limit(10); + 'pages' => [ + 'label' => I18n::translate('pages'), + 'icon' => 'page', + 'query' => function (string $query = null) { + $pages = App::instance()->site() + ->index(true) + ->search($query) + ->filter('isReadable', true) + ->limit(10); - $results = []; + $results = []; - foreach ($pages as $page) { - $results[] = [ - 'image' => $page->panel()->image(), - 'text' => Escape::html($page->title()->value()), - 'link' => $page->panel()->url(true), - 'info' => Escape::html($page->id()) - ]; - } + foreach ($pages as $page) { + $results[] = [ + 'image' => $page->panel()->image(), + 'text' => Escape::html($page->title()->value()), + 'link' => $page->panel()->url(true), + 'info' => Escape::html($page->id()) + ]; + } - return $results; - } - ], - 'files' => [ - 'label' => t('files'), - 'icon' => 'image', - 'query' => function (string $query = null) { - $files = site() - ->index(true) - ->filter('isReadable', true) - ->files() - ->search($query) - ->limit(10); + return $results; + } + ], + 'files' => [ + 'label' => I18n::translate('files'), + 'icon' => 'image', + 'query' => function (string $query = null) { + $files = App::instance()->site() + ->index(true) + ->filter('isReadable', true) + ->files() + ->search($query) + ->limit(10); - $results = []; + $results = []; - foreach ($files as $file) { - $results[] = [ - 'image' => $file->panel()->image(), - 'text' => Escape::html($file->filename()), - 'link' => $file->panel()->url(true), - 'info' => Escape::html($file->id()) - ]; - } + foreach ($files as $file) { + $results[] = [ + 'image' => $file->panel()->image(), + 'text' => Escape::html($file->filename()), + 'link' => $file->panel()->url(true), + 'info' => Escape::html($file->id()) + ]; + } - return $results; - } - ] + return $results; + } + ] ]; diff --git a/kirby/config/areas/site/views.php b/kirby/config/areas/site/views.php index eb6bdee..7465d2e 100644 --- a/kirby/config/areas/site/views.php +++ b/kirby/config/areas/site/views.php @@ -1,26 +1,27 @@ [ - 'pattern' => 'pages/(:any)', - 'action' => fn (string $path) => Find::page($path)->panel()->view() - ], - 'page.file' => [ - 'pattern' => 'pages/(:any)/files/(:any)', - 'action' => function (string $id, string $filename) { - return Find::file('pages/' . $id, $filename)->panel()->view(); - } - ], - 'site' => [ - 'pattern' => 'site', - 'action' => fn () => site()->panel()->view() - ], - 'site.file' => [ - 'pattern' => 'site/files/(:any)', - 'action' => function (string $filename) { - return Find::file('site', $filename)->panel()->view(); - } - ], + 'page' => [ + 'pattern' => 'pages/(:any)', + 'action' => fn (string $path) => Find::page($path)->panel()->view() + ], + 'page.file' => [ + 'pattern' => 'pages/(:any)/files/(:any)', + 'action' => function (string $id, string $filename) { + return Find::file('pages/' . $id, $filename)->panel()->view(); + } + ], + 'site' => [ + 'pattern' => 'site', + 'action' => fn () => App::instance()->site()->panel()->view() + ], + 'site.file' => [ + 'pattern' => 'site/files/(:any)', + 'action' => function (string $filename) { + return Find::file('site', $filename)->panel()->view(); + } + ], ]; diff --git a/kirby/config/areas/system.php b/kirby/config/areas/system.php index da7bccd..9f3075a 100644 --- a/kirby/config/areas/system.php +++ b/kirby/config/areas/system.php @@ -1,11 +1,13 @@ 'settings', - 'label' => t('view.system'), - 'menu' => true, - 'dialogs' => require __DIR__ . '/system/dialogs.php', - 'views' => require __DIR__ . '/system/views.php' - ]; + return [ + 'icon' => 'settings', + 'label' => I18n::translate('view.system'), + 'menu' => true, + 'dialogs' => require __DIR__ . '/system/dialogs.php', + 'views' => require __DIR__ . '/system/views.php' + ]; }; diff --git a/kirby/config/areas/system/dialogs.php b/kirby/config/areas/system/dialogs.php index 5566078..35a3f9a 100644 --- a/kirby/config/areas/system/dialogs.php +++ b/kirby/config/areas/system/dialogs.php @@ -1,43 +1,86 @@ [ - 'load' => function () { - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'license' => [ - 'label' => t('license.register.label'), - 'type' => 'text', - 'required' => true, - 'counter' => false, - 'placeholder' => 'K3-', - 'help' => t('license.register.help') - ], - 'email' => Field::email([ - 'required' => true - ]) - ], - 'submitButton' => t('license.register'), - 'value' => [ - 'license' => null, - 'email' => null - ] - ] - ]; - }, - 'submit' => function () { - // @codeCoverageIgnoreStart - kirby()->system()->register(get('license'), get('email')); - return [ - 'event' => 'system.register', - 'message' => t('license.register.success') - ]; - // @codeCoverageIgnoreEnd - } - ], + // license key + 'license' => [ + 'load' => function () { + $license = App::instance()->system()->license(); + + // @codeCoverageIgnoreStart + // the system is registered but the license + // key is only visible for admins + if ($license === true) { + $license = 'Kirby 3'; + } + // @codeCoverageIgnoreEnd + + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'size' => 'medium', + 'fields' => [ + 'license' => [ + 'type' => 'info', + 'label' => I18n::translate('license'), + 'text' => $license ? $license : I18n::translate('license.unregistered.label'), + 'theme' => $license ? 'code' : 'negative', + 'help' => $license ? + // @codeCoverageIgnoreStart + '' . I18n::translate('license.manage') . ' →' : + // @codeCoverageIgnoreEnd + '' . I18n::translate('license.buy') . ' →' + ] + ], + 'submitButton' => false, + 'cancelButton' => false, + ] + ]; + } + ], + // license registration + 'registration' => [ + 'load' => function () { + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'license' => [ + 'label' => I18n::translate('license.register.label'), + 'type' => 'text', + 'required' => true, + 'counter' => false, + 'placeholder' => 'K3-', + 'help' => I18n::translate('license.register.help') + ], + 'email' => Field::email([ + 'required' => true + ]) + ], + 'submitButton' => I18n::translate('license.register'), + 'value' => [ + 'license' => null, + 'email' => null + ] + ] + ]; + }, + 'submit' => function () { + // @codeCoverageIgnoreStart + $kirby = App::instance(); + $kirby->system()->register( + $kirby->request()->get('license'), + $kirby->request()->get('email') + ); + + return [ + 'event' => 'system.register', + 'message' => I18n::translate('license.register.success') + ]; + // @codeCoverageIgnoreEnd + } + ], ]; diff --git a/kirby/config/areas/system/views.php b/kirby/config/areas/system/views.php index 2fa2658..8bab2a2 100644 --- a/kirby/config/areas/system/views.php +++ b/kirby/config/areas/system/views.php @@ -1,47 +1,55 @@ [ - 'pattern' => 'system', - 'action' => function () { - $kirby = kirby(); - $system = $kirby->system(); - $license = $system->license(); + 'system' => [ + 'pattern' => 'system', + 'action' => function () { + $kirby = App::instance(); + $system = $kirby->system(); + $license = $system->license(); - // @codeCoverageIgnoreStart - if ($license === true) { - // valid license, but user is not admin - $license = 'Kirby 3'; - } elseif ($license === false) { - // no valid license - $license = null; - } - // @codeCoverageIgnoreEnd + // @codeCoverageIgnoreStart + if ($license === true) { + // valid license, but user is not admin + $license = 'Kirby 3'; + } elseif ($license === false) { + // no valid license + $license = null; + } + // @codeCoverageIgnoreEnd - $plugins = $system->plugins()->values(function ($plugin) { - return [ - 'author' => $plugin->authorsNames(), - 'license' => $plugin->license(), - 'link' => $plugin->link(), - 'name' => $plugin->name(), - 'version' => $plugin->version(), - ]; - }); + $plugins = $system->plugins()->values(function ($plugin) { + return [ + 'author' => $plugin->authorsNames(), + 'license' => $plugin->license(), + 'name' => [ + 'text' => $plugin->name(), + 'href' => $plugin->link(), + ], + 'version' => $plugin->version(), + ]; + }); - return [ - 'component' => 'k-system-view', - 'props' => [ - 'debug' => $kirby->option('debug', false), - 'license' => $license, - 'plugins' => $plugins, - 'php' => phpversion(), - 'server' => $system->serverSoftware(), - 'https' => Server::https(), - 'version' => $kirby->version(), - ] - ]; - } - ], + return [ + 'component' => 'k-system-view', + 'props' => [ + 'debug' => $kirby->option('debug', false), + 'license' => $license, + 'plugins' => $plugins, + 'php' => phpversion(), + 'server' => $system->serverSoftware(), + 'https' => $kirby->environment()->https(), + 'version' => $kirby->version(), + 'urls' => [ + 'content' => $system->exposedFileUrl('content'), + 'git' => $system->exposedFileUrl('git'), + 'kirby' => $system->exposedFileUrl('kirby'), + 'site' => $system->exposedFileUrl('site') + ] + ] + ]; + } + ], ]; diff --git a/kirby/config/areas/users.php b/kirby/config/areas/users.php index fd61535..ff7e130 100644 --- a/kirby/config/areas/users.php +++ b/kirby/config/areas/users.php @@ -1,14 +1,16 @@ 'users', - 'label' => t('view.users'), - 'search' => 'users', - 'menu' => true, - 'dialogs' => require __DIR__ . '/users/dialogs.php', - 'dropdowns' => require __DIR__ . '/users/dropdowns.php', - 'searches' => require __DIR__ . '/users/searches.php', - 'views' => require __DIR__ . '/users/views.php' - ]; + return [ + 'icon' => 'users', + 'label' => I18n::translate('view.users'), + 'search' => 'users', + 'menu' => true, + 'dialogs' => require __DIR__ . '/users/dialogs.php', + 'dropdowns' => require __DIR__ . '/users/dropdowns.php', + 'searches' => require __DIR__ . '/users/searches.php', + 'views' => require __DIR__ . '/users/views.php' + ]; }; diff --git a/kirby/config/areas/users/dialogs.php b/kirby/config/areas/users/dialogs.php index 2e8d9e6..291abb3 100644 --- a/kirby/config/areas/users/dialogs.php +++ b/kirby/config/areas/users/dialogs.php @@ -1,295 +1,311 @@ [ - 'pattern' => 'users/create', - 'load' => function () { - $kirby = kirby(); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'name' => Field::username(), - 'email' => Field::email([ - 'link' => false, - 'required' => true - ]), - 'password' => Field::password(), - 'translation' => Field::translation([ - 'required' => true - ]), - 'role' => Field::role([ - 'required' => true - ]) - ], - 'submitButton' => t('create'), - 'value' => [ - 'name' => '', - 'email' => '', - 'password' => '', - 'translation' => $kirby->panelLanguage(), - 'role' => $kirby->user()->role()->name() - ] - ] - ]; - }, - 'submit' => function () { - kirby()->users()->create([ - 'name' => get('name'), - 'email' => get('email'), - 'password' => get('password'), - 'language' => get('translation'), - 'role' => get('role') - ]); - return [ - 'event' => 'user.create' - ]; - } - ], + // create + 'user.create' => [ + 'pattern' => 'users/create', + 'load' => function () { + $kirby = App::instance(); + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'name' => Field::username(), + 'email' => Field::email([ + 'link' => false, + 'required' => true + ]), + 'password' => Field::password(), + 'translation' => Field::translation([ + 'required' => true + ]), + 'role' => Field::role([ + 'required' => true + ]) + ], + 'submitButton' => I18n::translate('create'), + 'value' => [ + 'name' => '', + 'email' => '', + 'password' => '', + 'translation' => $kirby->panelLanguage(), + 'role' => $kirby->user()->role()->name() + ] + ] + ]; + }, + 'submit' => function () { + $kirby = App::instance(); - // change email - 'user.changeEmail' => [ - 'pattern' => 'users/(:any)/changeEmail', - 'load' => function (string $id) { - $user = Find::user($id); + $kirby->users()->create([ + 'name' => $kirby->request()->get('name'), + 'email' => $kirby->request()->get('email'), + 'password' => $kirby->request()->get('password'), + 'language' => $kirby->request()->get('translation'), + 'role' => $kirby->request()->get('role') + ]); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'email' => [ - 'label' => t('email'), - 'required' => true, - 'type' => 'email', - 'preselect' => true - ] - ], - 'submitButton' => t('change'), - 'value' => [ - 'email' => $user->email() - ] - ] - ]; - }, - 'submit' => function (string $id) { - Find::user($id)->changeEmail(get('email')); - return [ - 'event' => 'user.changeEmail' - ]; - } - ], + return [ + 'event' => 'user.create' + ]; + } + ], - // change language - 'user.changeLanguage' => [ - 'pattern' => 'users/(:any)/changeLanguage', - 'load' => function (string $id) { - $user = Find::user($id); + // change email + 'user.changeEmail' => [ + 'pattern' => 'users/(:any)/changeEmail', + 'load' => function (string $id) { + $user = Find::user($id); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'translation' => Field::translation(['required' => true]) - ], - 'submitButton' => t('change'), - 'value' => [ - 'translation' => $user->language() - ] - ] - ]; - }, - 'submit' => function (string $id) { - Find::user($id)->changeLanguage(get('translation')); + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'email' => [ + 'label' => I18n::translate('email'), + 'required' => true, + 'type' => 'email', + 'preselect' => true + ] + ], + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'email' => $user->email() + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - return [ - 'event' => 'user.changeLanguage', - 'reload' => [ - 'globals' => '$translation' - ] - ]; - } - ], + Find::user($id)->changeEmail($request->get('email')); - // change name - 'user.changeName' => [ - 'pattern' => 'users/(:any)/changeName', - 'load' => function (string $id) { - $user = Find::user($id); + return [ + 'event' => 'user.changeEmail' + ]; + } + ], - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'name' => Field::username([ - 'preselect' => true - ]) - ], - 'submitButton' => t('rename'), - 'value' => [ - 'name' => $user->name()->value() - ] - ] - ]; - }, - 'submit' => function (string $id) { - Find::user($id)->changeName(get('name')); + // change language + 'user.changeLanguage' => [ + 'pattern' => 'users/(:any)/changeLanguage', + 'load' => function (string $id) { + $user = Find::user($id); - return [ - 'event' => 'user.changeName' - ]; - } - ], + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'translation' => Field::translation(['required' => true]) + ], + 'submitButton' => I18n::translate('change'), + 'value' => [ + 'translation' => $user->language() + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - // change password - 'user.changePassword' => [ - 'pattern' => 'users/(:any)/changePassword', - 'load' => function (string $id) { - $user = Find::user($id); + Find::user($id)->changeLanguage($request->get('translation')); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'password' => Field::password([ - 'label' => t('user.changePassword.new'), - ]), - 'passwordConfirmation' => Field::password([ - 'label' => t('user.changePassword.new.confirm'), - ]) - ], - 'submitButton' => t('change'), - ] - ]; - }, - 'submit' => function (string $id) { - $user = Find::user($id); - $password = get('password'); - $passwordConfirmation = get('passwordConfirmation'); + return [ + 'event' => 'user.changeLanguage', + 'reload' => [ + 'globals' => '$translation' + ] + ]; + } + ], - // validate the password - UserRules::validPassword($user, $password ?? ''); + // change name + 'user.changeName' => [ + 'pattern' => 'users/(:any)/changeName', + 'load' => function (string $id) { + $user = Find::user($id); - // compare passwords - if ($password !== $passwordConfirmation) { - throw new InvalidArgumentException([ - 'key' => 'user.password.notSame' - ]); - } + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'name' => Field::username([ + 'preselect' => true + ]) + ], + 'submitButton' => I18n::translate('rename'), + 'value' => [ + 'name' => $user->name()->value() + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - // change password if everything's fine - $user->changePassword($password); + Find::user($id)->changeName($request->get('name')); - return [ - 'event' => 'user.changePassword' - ]; - } - ], + return [ + 'event' => 'user.changeName' + ]; + } + ], - // change role - 'user.changeRole' => [ - 'pattern' => 'users/(:any)/changeRole', - 'load' => function (string $id) { - $user = Find::user($id); + // change password + 'user.changePassword' => [ + 'pattern' => 'users/(:any)/changePassword', + 'load' => function (string $id) { + $user = Find::user($id); - return [ - 'component' => 'k-form-dialog', - 'props' => [ - 'fields' => [ - 'role' => Field::role([ - 'label' => t('user.changeRole.select'), - 'required' => true, - ]) - ], - 'submitButton' => t('user.changeRole'), - 'value' => [ - 'role' => $user->role()->name() - ] - ] - ]; - }, - 'submit' => function (string $id) { - $user = Find::user($id)->changeRole(get('role')); + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'password' => Field::password([ + 'label' => I18n::translate('user.changePassword.new'), + ]), + 'passwordConfirmation' => Field::password([ + 'label' => I18n::translate('user.changePassword.new.confirm'), + ]) + ], + 'submitButton' => I18n::translate('change'), + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - return [ - 'event' => 'user.changeRole', - 'user' => $user->toArray() - ]; - } - ], + $user = Find::user($id); + $password = $request->get('password'); + $passwordConfirmation = $request->get('passwordConfirmation'); - // delete - 'user.delete' => [ - 'pattern' => 'users/(:any)/delete', - 'load' => function (string $id) { - $user = Find::user($id); - $i18nPrefix = $user->isLoggedIn() ? 'account' : 'user'; + // validate the password + UserRules::validPassword($user, $password ?? ''); - return [ - 'component' => 'k-remove-dialog', - 'props' => [ - 'text' => tt($i18nPrefix . '.delete.confirm', [ - 'email' => Escape::html($user->email()) - ]) - ] - ]; - }, - 'submit' => function (string $id) { - $user = Find::user($id); - $redirect = false; - $referrer = Panel::referrer(); - $url = $user->panel()->url(true); + // compare passwords + if ($password !== $passwordConfirmation) { + throw new InvalidArgumentException([ + 'key' => 'user.password.notSame' + ]); + } - $user->delete(); + // change password if everything's fine + $user->changePassword($password); - // redirect to the users view - // if the dialog has been opened in the user view - if ($referrer === $url) { - $redirect = '/users'; - } + return [ + 'event' => 'user.changePassword' + ]; + } + ], - // logout the user if they deleted themselves - if ($user->isLoggedIn()) { - $redirect = '/logout'; - } + // change role + 'user.changeRole' => [ + 'pattern' => 'users/(:any)/changeRole', + 'load' => function (string $id) { + $user = Find::user($id); - return [ - 'event' => 'user.delete', - 'dispatch' => ['content/remove' => [$url]], - 'redirect' => $redirect - ]; - } - ], + return [ + 'component' => 'k-form-dialog', + 'props' => [ + 'fields' => [ + 'role' => Field::role([ + 'label' => I18n::translate('user.changeRole.select'), + 'required' => true, + ]) + ], + 'submitButton' => I18n::translate('user.changeRole'), + 'value' => [ + 'role' => $user->role()->name() + ] + ] + ]; + }, + 'submit' => function (string $id) { + $request = App::instance()->request(); - // change file name - 'user.file.changeName' => [ - 'pattern' => '(users/.*?)/files/(:any)/changeName', - 'load' => $files['changeName']['load'], - 'submit' => $files['changeName']['submit'], - ], + $user = Find::user($id)->changeRole($request->get('role')); - // change file sort - 'user.file.changeSort' => [ - 'pattern' => '(users/.*?)/files/(:any)/changeSort', - 'load' => $files['changeSort']['load'], - 'submit' => $files['changeSort']['submit'], - ], + return [ + 'event' => 'user.changeRole', + 'user' => $user->toArray() + ]; + } + ], - // delete file - 'user.file.delete' => [ - 'pattern' => '(users/.*?)/files/(:any)/delete', - 'load' => $files['delete']['load'], - 'submit' => $files['delete']['submit'], - ] + // delete + 'user.delete' => [ + 'pattern' => 'users/(:any)/delete', + 'load' => function (string $id) { + $user = Find::user($id); + $i18nPrefix = $user->isLoggedIn() ? 'account' : 'user'; + + return [ + 'component' => 'k-remove-dialog', + 'props' => [ + 'text' => I18n::template($i18nPrefix . '.delete.confirm', [ + 'email' => Escape::html($user->email()) + ]) + ] + ]; + }, + 'submit' => function (string $id) { + $user = Find::user($id); + $redirect = false; + $referrer = Panel::referrer(); + $url = $user->panel()->url(true); + + $user->delete(); + + // redirect to the users view + // if the dialog has been opened in the user view + if ($referrer === $url) { + $redirect = '/users'; + } + + // logout the user if they deleted themselves + if ($user->isLoggedIn()) { + $redirect = '/logout'; + } + + return [ + 'event' => 'user.delete', + 'dispatch' => ['content/remove' => [$url]], + 'redirect' => $redirect + ]; + } + ], + + // change file name + 'user.file.changeName' => [ + 'pattern' => '(users/.*?)/files/(:any)/changeName', + 'load' => $files['changeName']['load'], + 'submit' => $files['changeName']['submit'], + ], + + // change file sort + 'user.file.changeSort' => [ + 'pattern' => '(users/.*?)/files/(:any)/changeSort', + 'load' => $files['changeSort']['load'], + 'submit' => $files['changeSort']['submit'], + ], + + // delete file + 'user.file.delete' => [ + 'pattern' => '(users/.*?)/files/(:any)/delete', + 'load' => $files['delete']['load'], + 'submit' => $files['delete']['submit'], + ] ]; diff --git a/kirby/config/areas/users/dropdowns.php b/kirby/config/areas/users/dropdowns.php index 2b3f15f..ec30a5f 100644 --- a/kirby/config/areas/users/dropdowns.php +++ b/kirby/config/areas/users/dropdowns.php @@ -5,14 +5,14 @@ use Kirby\Cms\Find; $files = require __DIR__ . '/../files/dropdowns.php'; return [ - 'user' => [ - 'pattern' => 'users/(:any)', - 'options' => function (string $id) { - return Find::user($id)->panel()->dropdown(); - } - ], - 'user.file' => [ - 'pattern' => '(users/.*?)/files/(:any)', - 'options' => $files['file'] - ] + 'user' => [ + 'pattern' => 'users/(:any)', + 'options' => function (string $id) { + return Find::user($id)->panel()->dropdown(); + } + ], + 'user.file' => [ + 'pattern' => '(users/.*?)/files/(:any)', + 'options' => $files['file'] + ] ]; diff --git a/kirby/config/areas/users/searches.php b/kirby/config/areas/users/searches.php index 879db7f..25a5702 100644 --- a/kirby/config/areas/users/searches.php +++ b/kirby/config/areas/users/searches.php @@ -1,25 +1,27 @@ [ - 'label' => t('users'), - 'icon' => 'users', - 'query' => function (string $query = null) { - $users = kirby()->users()->search($query)->limit(10); - $results = []; + 'users' => [ + 'label' => I18n::translate('users'), + 'icon' => 'users', + 'query' => function (string $query = null) { + $users = App::instance()->users()->search($query)->limit(10); + $results = []; - foreach ($users as $user) { - $results[] = [ - 'image' => $user->panel()->image(), - 'text' => Escape::html($user->username()), - 'link' => $user->panel()->url(true), - 'info' => Escape::html($user->role()->title()) - ]; - } + foreach ($users as $user) { + $results[] = [ + 'image' => $user->panel()->image(), + 'text' => Escape::html($user->username()), + 'link' => $user->panel()->url(true), + 'info' => Escape::html($user->role()->title()) + ]; + } - return $results; - } - ] + return $results; + } + ] ]; diff --git a/kirby/config/areas/users/views.php b/kirby/config/areas/users/views.php index 7d1a71b..a03a5d6 100644 --- a/kirby/config/areas/users/views.php +++ b/kirby/config/areas/users/views.php @@ -1,65 +1,66 @@ [ - 'pattern' => 'users', - 'action' => function () { - $kirby = kirby(); - $role = get('role'); - $roles = $kirby->roles()->toArray(fn ($role) => [ - 'id' => $role->id(), - 'title' => $role->title(), - ]); + 'users' => [ + 'pattern' => 'users', + 'action' => function () { + $kirby = App::instance(); + $role = $kirby->request()->get('role'); + $roles = $kirby->roles()->toArray(fn ($role) => [ + 'id' => $role->id(), + 'title' => $role->title(), + ]); - return [ - 'component' => 'k-users-view', - 'props' => [ - 'role' => function () use ($kirby, $roles, $role) { - if ($role) { - return $roles[$role] ?? null; - } - }, - 'roles' => array_values($roles), - 'users' => function () use ($kirby, $role) { - $users = $kirby->users(); + return [ + 'component' => 'k-users-view', + 'props' => [ + 'role' => function () use ($kirby, $roles, $role) { + if ($role) { + return $roles[$role] ?? null; + } + }, + 'roles' => array_values($roles), + 'users' => function () use ($kirby, $role) { + $users = $kirby->users(); - if (empty($role) === false) { - $users = $users->role($role); - } + if (empty($role) === false) { + $users = $users->role($role); + } - $users = $users->paginate([ - 'limit' => 20, - 'page' => get('page') - ]); + $users = $users->paginate([ + 'limit' => 20, + 'page' => $kirby->request()->get('page') + ]); - 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()) - ]), - 'pagination' => $users->pagination()->toArray() - ]; - }, - ] - ]; - } - ], - 'user' => [ - 'pattern' => 'users/(:any)', - 'action' => function (string $id) { - return Find::user($id)->panel()->view(); - } - ], - 'user.file' => [ - 'pattern' => 'users/(:any)/files/(:any)', - 'action' => function (string $id, string $filename) { - return Find::file('users/' . $id, $filename)->panel()->view(); - } - ], + 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()) + ]), + 'pagination' => $users->pagination()->toArray() + ]; + }, + ] + ]; + } + ], + 'user' => [ + 'pattern' => 'users/(:any)', + 'action' => function (string $id) { + return Find::user($id)->panel()->view(); + } + ], + 'user.file' => [ + 'pattern' => 'users/(:any)/files/(:any)', + 'action' => function (string $id, string $filename) { + return Find::file('users/' . $id, $filename)->panel()->view(); + } + ], ]; diff --git a/kirby/config/blocks/gallery/gallery.yml b/kirby/config/blocks/gallery/gallery.yml index a6844f2..168dbe9 100644 --- a/kirby/config/blocks/gallery/gallery.yml +++ b/kirby/config/blocks/gallery/gallery.yml @@ -5,6 +5,7 @@ fields: images: label: field.blocks.gallery.images.label type: files + query: model.images multiple: true layout: cards size: tiny diff --git a/kirby/config/blocks/image/image.php b/kirby/config/blocks/image/image.php index 17221c3..3e3e3df 100644 --- a/kirby/config/blocks/image/image.php +++ b/kirby/config/blocks/image/image.php @@ -9,17 +9,17 @@ $ratio = $block->ratio()->or('auto'); $src = null; if ($block->location() == 'web') { - $src = $block->src()->esc(); + $src = $block->src()->esc(); } elseif ($image = $block->image()->toFile()) { - $alt = $alt ?? $image->alt(); - $src = $image->url(); + $alt = $alt ?? $image->alt(); + $src = $image->url(); } ?> - $ratio, 'data-crop' => $crop], ' ') ?>> + $ratio, 'data-crop' => $crop], null, ' ') ?>> isNotEmpty()): ?> - + <?= $alt->esc() ?> diff --git a/kirby/config/blocks/image/image.yml b/kirby/config/blocks/image/image.yml index feff6b0..5909f8b 100644 --- a/kirby/config/blocks/image/image.yml +++ b/kirby/config/blocks/image/image.yml @@ -13,6 +13,7 @@ fields: image: label: field.blocks.image.name type: files + query: model.images multiple: false image: back: black diff --git a/kirby/config/blocks/video/video.php b/kirby/config/blocks/video/video.php index 9d0bfd3..1808946 100644 --- a/kirby/config/blocks/video/video.php +++ b/kirby/config/blocks/video/video.php @@ -1,5 +1,9 @@ - -url())): ?> + +url())): ?>
caption()->isNotEmpty()): ?> diff --git a/kirby/config/components.php b/kirby/config/components.php index 00341f2..914cc36 100644 --- a/kirby/config/components.php +++ b/kirby/config/components.php @@ -4,12 +4,12 @@ use Kirby\Cms\App; use Kirby\Cms\Collection; use Kirby\Cms\File; use Kirby\Cms\FileVersion; +use Kirby\Cms\Helpers; use Kirby\Cms\Template; use Kirby\Data\Data; use Kirby\Email\PHPMailer as Emailer; use Kirby\Filesystem\F; use Kirby\Filesystem\Filename; -use Kirby\Http\Server; use Kirby\Http\Uri; use Kirby\Http\Url; use Kirby\Image\Darkroom; @@ -21,380 +21,394 @@ use Kirby\Toolkit\Tpl as Snippet; return [ - /** - * Used by the `css()` helper - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string $url Relative or absolute URL - * @param string|array $options An array of attributes for the link tag or a media attribute string - */ - 'css' => fn (App $kirby, string $url, $options = null): string => $url, + /** + * Used by the `css()` helper + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $url Relative or absolute URL + * @param string|array $options An array of attributes for the link tag or a media attribute string + */ + 'css' => fn (App $kirby, string $url, $options = null): string => $url, - /** - * Object and variable dumper - * to help with debugging. - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param mixed $variable - * @param bool $echo - * @return string - */ - 'dump' => function (App $kirby, $variable, bool $echo = true) { - if (Server::cli() === true) { - $output = print_r($variable, true) . PHP_EOL; - } else { - $output = '
' . print_r($variable, true) . '
'; - } + /** + * Object and variable dumper + * to help with debugging. + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param mixed $variable + * @param bool $echo + * @return string + * + * @deprecated 3.7.0 Disable `dump()` via `KIRBY_HELPER_DUMP` instead and create your own function + * @todo move to `Helpers::dump()`, remove component in 3.8.0 + */ + 'dump' => function (App $kirby, $variable, bool $echo = true) { + if ($kirby->environment()->cli() === true) { + $output = print_r($variable, true) . PHP_EOL; + } else { + $output = '
' . print_r($variable, true) . '
'; + } - if ($echo === true) { - echo $output; - } + if ($echo === true) { + echo $output; + } - return $output; - }, + return $output; + }, - /** - * Add your own email provider - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param array $props - * @param bool $debug - */ - 'email' => function (App $kirby, array $props = [], bool $debug = false) { - return new Emailer($props, $debug); - }, + /** + * Add your own email provider + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param array $props + * @param bool $debug + */ + 'email' => function (App $kirby, array $props = [], bool $debug = false) { + return new Emailer($props, $debug); + }, - /** - * Modify URLs for file objects - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param \Kirby\Cms\File $file The original file object - * @return string - */ - 'file::url' => function (App $kirby, File $file): string { - return $file->mediaUrl(); - }, + /** + * Modify URLs for file objects + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param \Kirby\Cms\File $file The original file object + * @return string + */ + 'file::url' => function (App $kirby, File $file): string { + return $file->mediaUrl(); + }, - /** - * Adapt file characteristics - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param \Kirby\Cms\File|\Kirby\Filesystem\Asset $file The file object - * @param array $options All thumb options (width, height, crop, blur, grayscale) - * @return \Kirby\Cms\File|\Kirby\Cms\FileVersion|\Kirby\Filesystem\Asset - */ - 'file::version' => function (App $kirby, $file, array $options = []) { - // if file is not resizable, return - if ($file->isResizable() === false) { - return $file; - } + /** + * Adapt file characteristics + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param \Kirby\Cms\File|\Kirby\Filesystem\Asset $file The file object + * @param array $options All thumb options (width, height, crop, blur, grayscale) + * @return \Kirby\Cms\File|\Kirby\Cms\FileVersion|\Kirby\Filesystem\Asset + */ + 'file::version' => function (App $kirby, $file, array $options = []) { + // if file is not resizable, return + if ($file->isResizable() === false) { + return $file; + } - // create url and root - $mediaRoot = dirname($file->mediaRoot()); - $template = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}'; - $thumbRoot = (new Filename($file->root(), $template, $options))->toString(); - $thumbName = basename($thumbRoot); + // create url and root + $mediaRoot = dirname($file->mediaRoot()); + $template = $mediaRoot . '/{{ name }}{{ attributes }}.{{ extension }}'; + $thumbRoot = (new Filename($file->root(), $template, $options))->toString(); + $thumbName = basename($thumbRoot); - // check if the thumb already exists - if (file_exists($thumbRoot) === false) { + // check if the thumb already exists + if (file_exists($thumbRoot) === false) { + // if not, create job file + $job = $mediaRoot . '/.jobs/' . $thumbName . '.json'; - // if not, create job file - $job = $mediaRoot . '/.jobs/' . $thumbName . '.json'; + try { + Data::write($job, array_merge($options, [ + 'filename' => $file->filename() + ])); + } catch (Throwable $e) { + // if thumb doesn't exist yet and job file cannot + // be created, return + return $file; + } + } - try { - Data::write($job, array_merge($options, [ - 'filename' => $file->filename() - ])); - } catch (Throwable $e) { - // if thumb doesn't exist yet and job file cannot - // be created, return - return $file; - } - } + return new FileVersion([ + 'modifications' => $options, + 'original' => $file, + 'root' => $thumbRoot, + 'url' => dirname($file->mediaUrl()) . '/' . $thumbName, + ]); + }, - return new FileVersion([ - 'modifications' => $options, - 'original' => $file, - 'root' => $thumbRoot, - 'url' => dirname($file->mediaUrl()) . '/' . $thumbName, - ]); - }, + /** + * Used by the `js()` helper + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $url Relative or absolute URL + * @param string|array $options An array of attributes for the link tag or a media attribute string + */ + 'js' => fn (App $kirby, string $url, $options = null): string => $url, - /** - * Used by the `js()` helper - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string $url Relative or absolute URL - * @param string|array $options An array of attributes for the link tag or a media attribute string - */ - 'js' => fn (App $kirby, string $url, $options = null): string => $url, + /** + * Add your own Markdown parser + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $text Text to parse + * @param array $options Markdown options + * @param bool $inline Whether to wrap the text in `

` tags (deprecated: set via $options['inline'] instead) + * @return string + * @todo remove $inline parameter in in 3.8.0 + */ + 'markdown' => function (App $kirby, string $text = null, array $options = [], bool $inline = false): string { + static $markdown; + static $config; - /** - * Add your own Markdown parser - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string $text Text to parse - * @param array $options Markdown options - * @param bool $inline Whether to wrap the text in `

` tags (deprecated: set via $options['inline'] instead) - * @return string - * @todo add deprecation warning for $inline parameter in 3.7.0 - * @todo remove $inline parameter in in 3.8.0 - */ - 'markdown' => function (App $kirby, string $text = null, array $options = [], bool $inline = false): string { - static $markdown; - static $config; + // warning for deprecated fourth parameter + if (func_num_args() === 4 && isset($options['inline']) === false) { + // @codeCoverageIgnoreStart + Helpers::deprecated('markdown component: the $inline parameter is deprecated and will be removed in Kirby 3.8.0. Use $options[\'inline\'] instead.'); + // @codeCoverageIgnoreEnd + } - // support for the deprecated fourth argument - $options['inline'] ??= $inline; + // support for the deprecated fourth argument + $options['inline'] ??= $inline; - // if the config options have changed or the component is called for the first time, - // (re-)initialize the parser object - if ($config !== $options) { - $markdown = new Markdown($options); - $config = $options; - } + // if the config options have changed or the component is called for the first time, + // (re-)initialize the parser object + if ($config !== $options) { + $markdown = new Markdown($options); + $config = $options; + } - return $markdown->parse($text, $options['inline']); - }, + return $markdown->parse($text, $options['inline'] ?? false); + }, - /** - * Add your own search engine - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param \Kirby\Cms\Collection $collection Collection of searchable models - * @param string $query - * @param mixed $params - * @return \Kirby\Cms\Collection|bool - */ - 'search' => function (App $kirby, Collection $collection, string $query = null, $params = []) { - if (empty(trim($query)) === true) { - return $collection->limit(0); - } + /** + * Add your own search engine + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param \Kirby\Cms\Collection $collection Collection of searchable models + * @param string $query + * @param mixed $params + * @return \Kirby\Cms\Collection|bool + */ + 'search' => function (App $kirby, Collection $collection, string $query = null, $params = []) { + if (empty(trim($query ?? '')) === true) { + return $collection->limit(0); + } - if (is_string($params) === true) { - $params = ['fields' => Str::split($params, '|')]; - } + if (is_string($params) === true) { + $params = ['fields' => Str::split($params, '|')]; + } - $defaults = [ - 'fields' => [], - 'minlength' => 2, - 'score' => [], - 'words' => false, - ]; + $defaults = [ + 'fields' => [], + 'minlength' => 2, + 'score' => [], + 'words' => false, + ]; - $options = array_merge($defaults, $params); - $collection = clone $collection; - $searchWords = preg_replace('/(\s)/u', ',', $query); - $searchWords = Str::split($searchWords, ',', $options['minlength']); - $lowerQuery = Str::lower($query); - $exactQuery = $options['words'] ? '(\b' . preg_quote($query) . '\b)' : preg_quote($query); + $options = array_merge($defaults, $params); + $collection = clone $collection; + $searchWords = preg_replace('/(\s)/u', ',', $query); + $searchWords = Str::split($searchWords, ',', $options['minlength']); + $lowerQuery = Str::lower($query); + $exactQuery = $options['words'] ? '(\b' . preg_quote($query) . '\b)' : preg_quote($query); - if (empty($options['stopwords']) === false) { - $searchWords = array_diff($searchWords, $options['stopwords']); - } + if (empty($options['stopwords']) === false) { + $searchWords = array_diff($searchWords, $options['stopwords']); + } - $searchWords = array_map(function ($value) use ($options) { - return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value); - }, $searchWords); + $searchWords = array_map(function ($value) use ($options) { + return $options['words'] ? '\b' . preg_quote($value) . '\b' : preg_quote($value); + }, $searchWords); - $preg = '!(' . implode('|', $searchWords) . ')!i'; - $results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery, $exactQuery) { - $data = $item->content()->toArray(); - $keys = array_keys($data); - $keys[] = 'id'; + $preg = '!(' . implode('|', $searchWords) . ')!i'; + $results = $collection->filter(function ($item) use ($query, $preg, $options, $lowerQuery, $exactQuery) { + $data = $item->content()->toArray(); + $keys = array_keys($data); + $keys[] = 'id'; - if (is_a($item, 'Kirby\Cms\User') === true) { - $keys[] = 'name'; - $keys[] = 'email'; - $keys[] = 'role'; - } elseif (is_a($item, 'Kirby\Cms\Page') === true) { - // apply the default score for pages - $options['score'] = array_merge([ - 'id' => 64, - 'title' => 64, - ], $options['score']); - } + if (is_a($item, 'Kirby\Cms\User') === true) { + $keys[] = 'name'; + $keys[] = 'email'; + $keys[] = 'role'; + } elseif (is_a($item, 'Kirby\Cms\Page') === true) { + // apply the default score for pages + $options['score'] = array_merge([ + 'id' => 64, + 'title' => 64, + ], $options['score']); + } - if (empty($options['fields']) === false) { - $fields = array_map('strtolower', $options['fields']); - $keys = array_intersect($keys, $fields); - } + if (empty($options['fields']) === false) { + $fields = array_map('strtolower', $options['fields']); + $keys = array_intersect($keys, $fields); + } - $item->searchHits = 0; - $item->searchScore = 0; + $item->searchHits = 0; + $item->searchScore = 0; - foreach ($keys as $key) { - $score = $options['score'][$key] ?? 1; - $value = $data[$key] ?? (string)$item->$key(); + foreach ($keys as $key) { + $score = $options['score'][$key] ?? 1; + $value = $data[$key] ?? (string)$item->$key(); - $lowerValue = Str::lower($value); + $lowerValue = Str::lower($value); - // check for exact matches - if ($lowerQuery == $lowerValue) { - $item->searchScore += 16 * $score; - $item->searchHits += 1; + // check for exact matches + if ($lowerQuery == $lowerValue) { + $item->searchScore += 16 * $score; + $item->searchHits += 1; - // check for exact beginning matches - } elseif ($options['words'] === false && Str::startsWith($lowerValue, $lowerQuery) === true) { - $item->searchScore += 8 * $score; - $item->searchHits += 1; + // check for exact beginning matches + } elseif ($options['words'] === false && Str::startsWith($lowerValue, $lowerQuery) === true) { + $item->searchScore += 8 * $score; + $item->searchHits += 1; - // check for exact query matches - } elseif ($matches = preg_match_all('!' . $exactQuery . '!i', $value, $r)) { - $item->searchScore += 2 * $score; - $item->searchHits += $matches; - } + // check for exact query matches + } elseif ($matches = preg_match_all('!' . $exactQuery . '!i', $value, $r)) { + $item->searchScore += 2 * $score; + $item->searchHits += $matches; + } - // check for any match - if ($matches = preg_match_all($preg, $value, $r)) { - $item->searchHits += $matches; - $item->searchScore += $matches * $score; - } - } + // check for any match + if ($matches = preg_match_all($preg, $value, $r)) { + $item->searchHits += $matches; + $item->searchScore += $matches * $score; + } + } - return $item->searchHits > 0; - }); + return $item->searchHits > 0; + }); - return $results->sort('searchScore', 'desc'); - }, + return $results->sort('searchScore', 'desc'); + }, - /** - * Add your own SmartyPants parser - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string $text Text to parse - * @param array $options SmartyPants options - * @return string - */ - 'smartypants' => function (App $kirby, string $text = null, array $options = []): string { - static $smartypants; - static $config; + /** + * Add your own SmartyPants parser + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $text Text to parse + * @param array $options SmartyPants options + * @return string + */ + 'smartypants' => function (App $kirby, string $text = null, array $options = []): string { + static $smartypants; + static $config; - // if the config options have changed or the component is called for the first time, - // (re-)initialize the parser object - if ($config !== $options) { - $smartypants = new Smartypants($options); - $config = $options; - } + // if the config options have changed or the component is called for the first time, + // (re-)initialize the parser object + if ($config !== $options) { + $smartypants = new Smartypants($options); + $config = $options; + } - return $smartypants->parse($text); - }, + return $smartypants->parse($text); + }, - /** - * Add your own snippet loader - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string|array $name Snippet name - * @param array $data Data array for the snippet - * @return string|null - */ - 'snippet' => function (App $kirby, $name, array $data = []): ?string { - $snippets = A::wrap($name); + /** + * Add your own snippet loader + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string|array $name Snippet name + * @param array $data Data array for the snippet + * @return string|null + */ + 'snippet' => function (App $kirby, $name, array $data = []): ?string { + $snippets = A::wrap($name); - foreach ($snippets as $name) { - $name = (string)$name; - $file = $kirby->root('snippets') . '/' . $name . '.php'; + foreach ($snippets as $name) { + $name = (string)$name; + $file = $kirby->root('snippets') . '/' . $name . '.php'; - if (file_exists($file) === false) { - $file = $kirby->extensions('snippets')[$name] ?? null; - } + if (file_exists($file) === false) { + $file = $kirby->extensions('snippets')[$name] ?? null; + } - if ($file) { - break; - } - } + if ($file) { + break; + } + } - return Snippet::load($file, $data); - }, + return Snippet::load($file, $data); + }, - /** - * Add your own template engine - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string $name Template name - * @param string $type Extension type - * @param string $defaultType Default extension type - * @return \Kirby\Cms\Template - */ - 'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') { - return new Template($name, $type, $defaultType); - }, + /** + * Add your own template engine + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $name Template name + * @param string $type Extension type + * @param string $defaultType Default extension type + * @return \Kirby\Cms\Template + */ + 'template' => function (App $kirby, string $name, string $type = 'html', string $defaultType = 'html') { + return new Template($name, $type, $defaultType); + }, - /** - * Add your own thumb generator - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string $src Root of the original file - * @param string $dst Template string for the root to the desired destination - * @param array $options All thumb options that should be applied: `width`, `height`, `crop`, `blur`, `grayscale` - * @return string - */ - 'thumb' => function (App $kirby, string $src, string $dst, array $options): string { - $darkroom = Darkroom::factory( - option('thumbs.driver', 'gd'), - option('thumbs', []) - ); - $options = $darkroom->preprocess($src, $options); - $root = (new Filename($src, $dst, $options))->toString(); + /** + * Add your own thumb generator + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string $src Root of the original file + * @param string $dst Template string for the root to the desired destination + * @param array $options All thumb options that should be applied: `width`, `height`, `crop`, `blur`, `grayscale` + * @return string + */ + 'thumb' => function (App $kirby, string $src, string $dst, array $options): string { + $darkroom = Darkroom::factory( + $kirby->option('thumbs.driver', 'gd'), + $kirby->option('thumbs', []) + ); + $options = $darkroom->preprocess($src, $options); + $root = (new Filename($src, $dst, $options))->toString(); - F::copy($src, $root, true); - $darkroom->process($root, $options); + F::copy($src, $root, true); + $darkroom->process($root, $options); - return $root; - }, + return $root; + }, - /** - * Modify all URLs - * - * @param \Kirby\Cms\App $kirby Kirby instance - * @param string|null $path URL path - * @param array|string|null $options Array of options for the Uri class - * @return string - */ - 'url' => function (App $kirby, string $path = null, $options = null): string { - $language = null; + /** + * Modify all URLs + * + * @param \Kirby\Cms\App $kirby Kirby instance + * @param string|null $path URL path + * @param array|string|null $options Array of options for the Uri class + * @return string + */ + 'url' => function (App $kirby, string $path = null, $options = null): string { + $language = null; - // get language from simple string option - if (is_string($options) === true) { - $language = $options; - $options = null; - } + // get language from simple string option + if (is_string($options) === true) { + $language = $options; + $options = null; + } - // get language from array - if (is_array($options) === true && isset($options['language']) === true) { - $language = $options['language']; - unset($options['language']); - } + // get language from array + if (is_array($options) === true && isset($options['language']) === true) { + $language = $options['language']; + unset($options['language']); + } - // get a language url for the linked page, if the page can be found - if ($kirby->multilang() === true) { - $parts = Str::split($path, '#'); + // get a language url for the linked page, if the page can be found + if ($kirby->multilang() === true) { + $parts = Str::split($path, '#'); - if ($page = page($parts[0] ?? null)) { - $path = $page->url($language); + if ($parts[0] ?? null) { + $page = $kirby->site()->find($parts[0]); + } else { + $page = $kirby->site()->page(); + } - if (isset($parts[1]) === true) { - $path .= '#' . $parts[1]; - } - } - } + if ($page) { + $path = $page->url($language); - // keep relative urls - if ( - $path !== null && - (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') - ) { - return $path; - } + if (isset($parts[1]) === true) { + $path .= '#' . $parts[1]; + } + } + } - $url = Url::makeAbsolute($path, $kirby->url()); + // keep relative urls + if ( + $path !== null && + (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') + ) { + return $path; + } - if ($options === null) { - return $url; - } + $url = Url::makeAbsolute($path, $kirby->url()); - return (new Uri($url, $options))->toString(); - }, + if ($options === null) { + return $url; + } + + return (new Uri($url, $options))->toString(); + }, ]; diff --git a/kirby/config/fields/checkboxes.php b/kirby/config/fields/checkboxes.php index 6837b45..c8d962d 100644 --- a/kirby/config/fields/checkboxes.php +++ b/kirby/config/fields/checkboxes.php @@ -4,58 +4,58 @@ use Kirby\Toolkit\A; use Kirby\Toolkit\Str; return [ - 'mixins' => ['min', 'options'], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'before' => null, - 'icon' => null, - 'placeholder' => null, + 'mixins' => ['min', 'options'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, - /** - * Arranges the checkboxes in the given number of columns - */ - 'columns' => function (int $columns = 1) { - return $columns; - }, - /** - * Default value for the field, which will be used when a page/file/user is created - */ - 'default' => function ($default = null) { - return Str::split($default, ','); - }, - /** - * Maximum number of checked boxes - */ - 'max' => function (int $max = null) { - return $max; - }, - /** - * Minimum number of checked boxes - */ - 'min' => function (int $min = null) { - return $min; - }, - 'value' => function ($value = null) { - return Str::split($value, ','); - }, - ], - 'computed' => [ - 'default' => function () { - return $this->sanitizeOptions($this->default); - }, - 'value' => function () { - return $this->sanitizeOptions($this->value); - }, - ], - 'save' => function ($value): string { - return A::join($value, ', '); - }, - 'validations' => [ - 'options', - 'max', - 'min' - ] + /** + * Arranges the checkboxes in the given number of columns + */ + 'columns' => function (int $columns = 1) { + return $columns; + }, + /** + * Default value for the field, which will be used when a page/file/user is created + */ + 'default' => function ($default = null) { + return Str::split($default, ','); + }, + /** + * Maximum number of checked boxes + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * Minimum number of checked boxes + */ + 'min' => function (int $min = null) { + return $min; + }, + 'value' => function ($value = null) { + return Str::split($value, ','); + }, + ], + 'computed' => [ + 'default' => function () { + return $this->sanitizeOptions($this->default); + }, + 'value' => function () { + return $this->sanitizeOptions($this->value); + }, + ], + 'save' => function ($value): string { + return A::join($value, ', '); + }, + 'validations' => [ + 'options', + 'max', + 'min' + ] ]; diff --git a/kirby/config/fields/date.php b/kirby/config/fields/date.php index e2124c4..ffb6dc4 100644 --- a/kirby/config/fields/date.php +++ b/kirby/config/fields/date.php @@ -7,148 +7,148 @@ use Kirby\Toolkit\I18n; use Kirby\Toolkit\Str; return [ - 'mixins' => ['datetime'], - 'props' => [ - /** - * Unset inherited props - */ - 'placeholder' => null, + 'mixins' => ['datetime'], + 'props' => [ + /** + * Unset inherited props + */ + 'placeholder' => null, - /** - * Activate/deactivate the dropdown calendar - */ - 'calendar' => function (bool $calendar = true) { - return $calendar; - }, + /** + * Activate/deactivate the dropdown calendar + */ + 'calendar' => function (bool $calendar = true) { + return $calendar; + }, - /** - * Default date when a new page/file/user gets created - */ - 'default' => function (string $default = null): string { - return $this->toDatetime($default) ?? ''; - }, + /** + * Default date when a new page/file/user gets created + */ + 'default' => function (string $default = null): string { + return $this->toDatetime($default) ?? ''; + }, - /** - * Custom format (dayjs tokens: `DD`, `MM`, `YYYY`) that is - * used to display the field in the Panel - */ - 'display' => function ($display = 'YYYY-MM-DD') { - return I18n::translate($display, $display); - }, + /** + * Custom format (dayjs tokens: `DD`, `MM`, `YYYY`) that is + * used to display the field in the Panel + */ + 'display' => function ($display = 'YYYY-MM-DD') { + return I18n::translate($display, $display); + }, - /** - * Changes the calendar icon to something custom - */ - 'icon' => function (string $icon = 'calendar') { - return $icon; - }, + /** + * Changes the calendar icon to something custom + */ + 'icon' => function (string $icon = 'calendar') { + return $icon; + }, - /** - * Latest date, which can be selected/saved (Y-m-d) - */ - 'max' => function (string $max = null): ?string { - return Date::optional($max); - }, - /** - * Earliest date, which can be selected/saved (Y-m-d) - */ - 'min' => function (string $min = null): ?string { - return Date::optional($min); - }, + /** + * Latest date, which can be selected/saved (Y-m-d) + */ + 'max' => function (string $max = null): ?string { + return Date::optional($max); + }, + /** + * Earliest date, which can be selected/saved (Y-m-d) + */ + 'min' => function (string $min = null): ?string { + return Date::optional($min); + }, - /** - * Round to the nearest: sub-options for `unit` (day) and `size` (1) - */ - 'step' => function ($step = null) { - return $step; - }, + /** + * Round to the nearest: sub-options for `unit` (day) and `size` (1) + */ + 'step' => function ($step = null) { + return $step; + }, - /** - * Pass `true` or an array of time field options to show the time selector. - */ - 'time' => function ($time = false) { - return $time; - }, - /** - * Must be a parseable date string - */ - 'value' => function ($value = null) { - return $value; - } - ], - 'computed' => [ - 'display' => function () { - if ($this->display) { - return Str::upper($this->display); - } - }, - 'format' => function () { - return $this->props['format'] ?? ($this->time === false ? 'Y-m-d' : 'Y-m-d H:i:s'); - }, - 'time' => function () { - if ($this->time === false) { - return false; - } + /** + * Pass `true` or an array of time field options to show the time selector. + */ + 'time' => function ($time = false) { + return $time; + }, + /** + * Must be a parseable date string + */ + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'display' => function () { + if ($this->display) { + return Str::upper($this->display); + } + }, + 'format' => function () { + return $this->props['format'] ?? ($this->time === false ? 'Y-m-d' : 'Y-m-d H:i:s'); + }, + 'time' => function () { + if ($this->time === false) { + return false; + } - $props = is_array($this->time) ? $this->time : []; - $props['model'] = $this->model(); - $field = new Field('time', $props); - return $field->toArray(); - }, - 'step' => function () { - if ($this->time === false || empty($this->time['step']) === true) { - return Date::stepConfig($this->step, [ - 'size' => 1, - 'unit' => 'day' - ]); - } + $props = is_array($this->time) ? $this->time : []; + $props['model'] = $this->model(); + $field = new Field('time', $props); + return $field->toArray(); + }, + 'step' => function () { + if ($this->time === false || empty($this->time['step']) === true) { + return Date::stepConfig($this->step, [ + 'size' => 1, + 'unit' => 'day' + ]); + } - return Date::stepConfig($this->time['step'], [ - 'size' => 5, - 'unit' => 'minute' - ]); - }, - 'value' => function (): string { - return $this->toDatetime($this->value) ?? ''; - }, - ], - 'validations' => [ - 'date', - 'minMax' => function ($value) { - if (!$value = Date::optional($value)) { - return true; - } + return Date::stepConfig($this->time['step'], [ + 'size' => 5, + 'unit' => 'minute' + ]); + }, + 'value' => function (): string { + return $this->toDatetime($this->value) ?? ''; + }, + ], + 'validations' => [ + 'date', + 'minMax' => function ($value) { + if (!$value = Date::optional($value)) { + return true; + } - $min = Date::optional($this->min); - $max = Date::optional($this->max); + $min = Date::optional($this->min); + $max = Date::optional($this->max); - $format = $this->time === false ? 'd.m.Y' : 'd.m.Y H:i'; + $format = $this->time === false ? 'd.m.Y' : 'd.m.Y H:i'; - if ($min && $max && $value->isBetween($min, $max) === false) { - throw new Exception([ - 'key' => 'validation.date.between', - 'data' => [ - 'min' => $min->format($format), - 'max' => $min->format($format) - ] - ]); - } elseif ($min && $value->isMin($min) === false) { - throw new Exception([ - 'key' => 'validation.date.after', - 'data' => [ - 'date' => $min->format($format), - ] - ]); - } elseif ($max && $value->isMax($max) === false) { - throw new Exception([ - 'key' => 'validation.date.before', - 'data' => [ - 'date' => $max->format($format), - ] - ]); - } + if ($min && $max && $value->isBetween($min, $max) === false) { + throw new Exception([ + 'key' => 'validation.date.between', + 'data' => [ + 'min' => $min->format($format), + 'max' => $min->format($format) + ] + ]); + } elseif ($min && $value->isMin($min) === false) { + throw new Exception([ + 'key' => 'validation.date.after', + 'data' => [ + 'date' => $min->format($format), + ] + ]); + } elseif ($max && $value->isMax($max) === false) { + throw new Exception([ + 'key' => 'validation.date.before', + 'data' => [ + 'date' => $max->format($format), + ] + ]); + } - return true; - }, - ] + return true; + }, + ] ]; diff --git a/kirby/config/fields/email.php b/kirby/config/fields/email.php index e7892b8..5c4630f 100644 --- a/kirby/config/fields/email.php +++ b/kirby/config/fields/email.php @@ -3,38 +3,38 @@ use Kirby\Toolkit\I18n; return [ - 'extends' => 'text', - 'props' => [ - /** - * Unset inherited props - */ - 'converter' => null, - 'counter' => null, + 'extends' => 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, - /** - * Sets the HTML5 autocomplete mode for the input - */ - 'autocomplete' => function (string $autocomplete = 'email') { - return $autocomplete; - }, + /** + * Sets the HTML5 autocomplete mode for the input + */ + 'autocomplete' => function (string $autocomplete = 'email') { + return $autocomplete; + }, - /** - * Changes the email icon to something custom - */ - 'icon' => function (string $icon = 'email') { - return $icon; - }, + /** + * Changes the email icon to something custom + */ + 'icon' => function (string $icon = 'email') { + return $icon; + }, - /** - * Custom placeholder text, when the field is empty. - */ - 'placeholder' => function ($value = null) { - return I18n::translate($value, $value) ?? I18n::translate('email.placeholder'); - } - ], - 'validations' => [ - 'minlength', - 'maxlength', - 'email' - ] + /** + * Custom placeholder text, when the field is empty. + */ + 'placeholder' => function ($value = null) { + return I18n::translate($value, $value) ?? I18n::translate('email.placeholder'); + } + ], + 'validations' => [ + 'minlength', + 'maxlength', + 'email' + ] ]; diff --git a/kirby/config/fields/files.php b/kirby/config/fields/files.php index 10fa851..5fef3e2 100644 --- a/kirby/config/fields/files.php +++ b/kirby/config/fields/files.php @@ -4,128 +4,128 @@ use Kirby\Data\Data; use Kirby\Toolkit\A; return [ - 'mixins' => [ - 'filepicker', - 'layout', - 'min', - 'picker', - 'upload' - ], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'before' => null, - 'autofocus' => null, - 'icon' => null, - 'placeholder' => null, + 'mixins' => [ + 'filepicker', + 'layout', + 'min', + 'picker', + 'upload' + ], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'autofocus' => null, + 'icon' => null, + 'placeholder' => null, - /** - * Sets the file(s), which are selected by default when a new page is created - */ - 'default' => function ($default = null) { - return $default; - }, + /** + * Sets the file(s), which are selected by default when a new page is created + */ + 'default' => function ($default = null) { + return $default; + }, - 'value' => function ($value = null) { - return $value; - } - ], - 'computed' => [ - 'parentModel' => function () { - if (is_string($this->parent) === true && $model = $this->model()->query($this->parent, 'Kirby\Cms\Model')) { - return $model; - } + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'parentModel' => function () { + if (is_string($this->parent) === true && $model = $this->model()->query($this->parent, 'Kirby\Cms\Model')) { + return $model; + } - return $this->model(); - }, - 'parent' => function () { - return $this->parentModel->apiUrl(true); - }, - 'query' => function () { - return $this->query ?? $this->parentModel::CLASS_ALIAS . '.files'; - }, - 'default' => function () { - return $this->toFiles($this->default); - }, - 'value' => function () { - return $this->toFiles($this->value); - }, - ], - 'methods' => [ - 'fileResponse' => function ($file) { - return $file->panel()->pickerData([ - 'image' => $this->image, - 'info' => $this->info ?? false, - 'layout' => $this->layout, - 'model' => $this->model(), - 'text' => $this->text, - ]); - }, - 'toFiles' => function ($value = null) { - $files = []; + return $this->model(); + }, + 'parent' => function () { + return $this->parentModel->apiUrl(true); + }, + 'query' => function () { + return $this->query ?? $this->parentModel::CLASS_ALIAS . '.files'; + }, + 'default' => function () { + return $this->toFiles($this->default); + }, + 'value' => function () { + return $this->toFiles($this->value); + }, + ], + 'methods' => [ + 'fileResponse' => function ($file) { + return $file->panel()->pickerData([ + 'image' => $this->image, + 'info' => $this->info ?? false, + 'layout' => $this->layout, + 'model' => $this->model(), + 'text' => $this->text, + ]); + }, + 'toFiles' => function ($value = null) { + $files = []; - foreach (Data::decode($value, 'yaml') as $id) { - if (is_array($id) === true) { - $id = $id['id'] ?? null; - } + foreach (Data::decode($value, 'yaml') as $id) { + if (is_array($id) === true) { + $id = $id['id'] ?? null; + } - if ($id !== null && ($file = $this->kirby()->file($id, $this->model()))) { - $files[] = $this->fileResponse($file); - } - } + if ($id !== null && ($file = $this->kirby()->file($id, $this->model()))) { + $files[] = $this->fileResponse($file); + } + } - return $files; - } - ], - 'api' => function () { - return [ - [ - 'pattern' => '/', - 'action' => function () { - $field = $this->field(); + return $files; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); - return $field->filepicker([ - 'image' => $field->image(), - 'info' => $field->info(), - 'layout' => $field->layout(), - 'limit' => $field->limit(), - 'page' => $this->requestQuery('page'), - 'query' => $field->query(), - 'search' => $this->requestQuery('search'), - 'text' => $field->text() - ]); - } - ], - [ - 'pattern' => 'upload', - 'method' => 'POST', - 'action' => function () { - $field = $this->field(); - $uploads = $field->uploads(); + return $field->filepicker([ + 'image' => $field->image(), + 'info' => $field->info(), + 'layout' => $field->layout(), + 'limit' => $field->limit(), + 'page' => $this->requestQuery('page'), + 'query' => $field->query(), + 'search' => $this->requestQuery('search'), + 'text' => $field->text() + ]); + } + ], + [ + 'pattern' => 'upload', + 'method' => 'POST', + 'action' => function () { + $field = $this->field(); + $uploads = $field->uploads(); - // move_uploaded_file() not working with unit test - // @codeCoverageIgnoreStart - return $field->upload($this, $uploads, function ($file, $parent) use ($field) { - return $file->panel()->pickerData([ - 'image' => $field->image(), - 'info' => $field->info(), - 'layout' => $field->layout(), - 'model' => $field->model(), - 'text' => $field->text(), - ]); - }); - // @codeCoverageIgnoreEnd - } - ] - ]; - }, - 'save' => function ($value = null) { - return A::pluck($value, 'uuid'); - }, - 'validations' => [ - 'max', - 'min' - ] + // move_uploaded_file() not working with unit test + // @codeCoverageIgnoreStart + return $field->upload($this, $uploads, function ($file, $parent) use ($field) { + return $file->panel()->pickerData([ + 'image' => $field->image(), + 'info' => $field->info(), + 'layout' => $field->layout(), + 'model' => $field->model(), + 'text' => $field->text(), + ]); + }); + // @codeCoverageIgnoreEnd + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'uuid'); + }, + 'validations' => [ + 'max', + 'min' + ] ]; diff --git a/kirby/config/fields/gap.php b/kirby/config/fields/gap.php index 6844d6c..b2dbd70 100644 --- a/kirby/config/fields/gap.php +++ b/kirby/config/fields/gap.php @@ -1,5 +1,5 @@ false + 'save' => false ]; diff --git a/kirby/config/fields/headline.php b/kirby/config/fields/headline.php index c87dd53..01994ad 100644 --- a/kirby/config/fields/headline.php +++ b/kirby/config/fields/headline.php @@ -1,26 +1,26 @@ false, - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'autofocus' => null, - 'before' => null, - 'default' => null, - 'disabled' => null, - 'icon' => null, - 'placeholder' => null, - 'required' => null, - 'translate' => null, + 'save' => false, + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'default' => null, + 'disabled' => null, + 'icon' => null, + 'placeholder' => null, + 'required' => null, + 'translate' => null, - /** - * If `false`, the prepended number will be hidden - */ - 'numbered' => function (bool $numbered = true) { - return $numbered; - } - ] + /** + * If `false`, the prepended number will be hidden + */ + 'numbered' => function (bool $numbered = true) { + return $numbered; + } + ] ]; diff --git a/kirby/config/fields/info.php b/kirby/config/fields/info.php index dcf174b..4df8ed3 100644 --- a/kirby/config/fields/info.php +++ b/kirby/config/fields/info.php @@ -3,42 +3,42 @@ use Kirby\Toolkit\I18n; return [ - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'autofocus' => null, - 'before' => null, - 'default' => null, - 'disabled' => null, - 'icon' => null, - 'placeholder' => null, - 'required' => null, - 'translate' => null, + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'default' => null, + 'disabled' => null, + 'icon' => null, + 'placeholder' => null, + 'required' => null, + 'translate' => null, - /** - * Text to be displayed - */ - 'text' => function ($value = null) { - return I18n::translate($value, $value); - }, + /** + * Text to be displayed + */ + 'text' => function ($value = null) { + return I18n::translate($value, $value); + }, - /** - * Change the design of the info box - */ - 'theme' => function (string $theme = null) { - return $theme; - } - ], - 'computed' => [ - 'text' => function () { - if ($text = $this->text) { - $text = $this->model()->toSafeString($text); - $text = $this->kirby()->kirbytext($text); - return $text; - } - } - ], - 'save' => false, + /** + * Change the design of the info box + */ + 'theme' => function (string $theme = null) { + return $theme; + } + ], + 'computed' => [ + 'text' => function () { + if ($text = $this->text) { + $text = $this->model()->toSafeString($text); + $text = $this->kirby()->kirbytext($text); + return $text; + } + } + ], + 'save' => false, ]; diff --git a/kirby/config/fields/line.php b/kirby/config/fields/line.php index 6844d6c..b2dbd70 100644 --- a/kirby/config/fields/line.php +++ b/kirby/config/fields/line.php @@ -1,5 +1,5 @@ false + 'save' => false ]; diff --git a/kirby/config/fields/list.php b/kirby/config/fields/list.php index c4d886f..74493a7 100644 --- a/kirby/config/fields/list.php +++ b/kirby/config/fields/list.php @@ -1,17 +1,17 @@ [ - /** - * Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`. Activate them all by passing `true`. Deactivate them all by passing `false` - */ - 'marks' => function ($marks = true) { - return $marks; - } - ], - 'computed' => [ - 'value' => function () { - return trim($this->value ?? ''); - } - ] + 'props' => [ + /** + * Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`. Activate them all by passing `true`. Deactivate them all by passing `false` + */ + 'marks' => function ($marks = true) { + return $marks; + } + ], + 'computed' => [ + 'value' => function () { + return trim($this->value ?? ''); + } + ] ]; diff --git a/kirby/config/fields/mixins/datetime.php b/kirby/config/fields/mixins/datetime.php index 8a2125f..b47a865 100644 --- a/kirby/config/fields/mixins/datetime.php +++ b/kirby/config/fields/mixins/datetime.php @@ -3,33 +3,33 @@ use Kirby\Toolkit\Date; return [ - 'props' => [ - /** - * Defines a custom format that is used when the field is saved - */ - 'format' => function (string $format = null) { - return $format; - } - ], - 'methods' => [ - 'toDatetime' => function ($value, string $format = 'Y-m-d H:i:s') { - if ($date = Date::optional($value)) { - if ($this->step) { - $step = Date::stepConfig($this->step); - $date->round($step['unit'], $step['size']); - } + 'props' => [ + /** + * Defines a custom format that is used when the field is saved + */ + 'format' => function (string $format = null) { + return $format; + } + ], + 'methods' => [ + 'toDatetime' => function ($value, string $format = 'Y-m-d H:i:s') { + if ($date = Date::optional($value)) { + if ($this->step) { + $step = Date::stepConfig($this->step); + $date->round($step['unit'], $step['size']); + } - return $date->format($format); - } + return $date->format($format); + } - return null; - } - ], - 'save' => function ($value) { - if ($date = Date::optional($value)) { - return $date->format($this->format); - } + return null; + } + ], + 'save' => function ($value) { + if ($date = Date::optional($value)) { + return $date->format($this->format); + } - return ''; - }, + return ''; + }, ]; diff --git a/kirby/config/fields/mixins/filepicker.php b/kirby/config/fields/mixins/filepicker.php index ba81230..092adc9 100644 --- a/kirby/config/fields/mixins/filepicker.php +++ b/kirby/config/fields/mixins/filepicker.php @@ -3,12 +3,12 @@ use Kirby\Cms\FilePicker; return [ - 'methods' => [ - 'filepicker' => function (array $params = []) { - // fetch the parent model - $params['model'] = $this->model(); + 'methods' => [ + 'filepicker' => function (array $params = []) { + // fetch the parent model + $params['model'] = $this->model(); - return (new FilePicker($params))->toArray(); - } - ] + return (new FilePicker($params))->toArray(); + } + ] ]; diff --git a/kirby/config/fields/mixins/layout.php b/kirby/config/fields/mixins/layout.php index 3fdb1eb..4ac0138 100644 --- a/kirby/config/fields/mixins/layout.php +++ b/kirby/config/fields/mixins/layout.php @@ -1,21 +1,21 @@ [ - /** - * Changes the layout of the selected entries. - * Available layouts: `list`, `cardlets`, `cards` - */ - 'layout' => function (string $layout = 'list') { - $layouts = ['list', 'cardlets', 'cards']; - return in_array($layout, $layouts) ? $layout : 'list'; - }, + 'props' => [ + /** + * Changes the layout of the selected entries. + * Available layouts: `list`, `cardlets`, `cards` + */ + 'layout' => function (string $layout = 'list') { + $layouts = ['list', 'cardlets', 'cards']; + return in_array($layout, $layouts) ? $layout : 'list'; + }, - /** - * Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge` - */ - 'size' => function (string $size = 'auto') { - return $size; - }, - ] + /** + * Layout size for cards: `tiny`, `small`, `medium`, `large` or `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + ] ]; diff --git a/kirby/config/fields/mixins/min.php b/kirby/config/fields/mixins/min.php index 33e24d4..f5262ea 100644 --- a/kirby/config/fields/mixins/min.php +++ b/kirby/config/fields/mixins/min.php @@ -1,22 +1,22 @@ [ - 'min' => function () { - // set min to at least 1, if required - if ($this->required === true) { - return $this->min ?? 1; - } + 'computed' => [ + 'min' => function () { + // set min to at least 1, if required + if ($this->required === true) { + return $this->min ?? 1; + } - return $this->min; - }, - 'required' => function () { - // set required to true if min is set - if ($this->min) { - return true; - } + return $this->min; + }, + 'required' => function () { + // set required to true if min is set + if ($this->min) { + return true; + } - return $this->required; - } - ] + return $this->required; + } + ] ]; diff --git a/kirby/config/fields/mixins/options.php b/kirby/config/fields/mixins/options.php index 170761a..465ac50 100644 --- a/kirby/config/fields/mixins/options.php +++ b/kirby/config/fields/mixins/options.php @@ -3,46 +3,46 @@ use Kirby\Form\Options; return [ - 'props' => [ - /** - * API settings for options requests. This will only take affect when `options` is set to `api`. - */ - 'api' => function ($api = null) { - return $api; - }, - /** - * An array with options - */ - 'options' => function ($options = []) { - return $options; - }, - /** - * Query settings for options queries. This will only take affect when `options` is set to `query`. - */ - 'query' => function ($query = null) { - return $query; - }, - ], - 'computed' => [ - 'options' => function (): array { - return $this->getOptions(); - } - ], - 'methods' => [ - 'getOptions' => function () { - return Options::factory( - $this->options(), - $this->props, - $this->model() - ); - }, - 'sanitizeOption' => function ($option) { - $allowed = array_column($this->options(), 'value'); - return in_array($option, $allowed, true) === true ? $option : null; - }, - 'sanitizeOptions' => function ($options) { - $allowed = array_column($this->options(), 'value'); - return array_intersect($options, $allowed); - }, - ] + 'props' => [ + /** + * API settings for options requests. This will only take affect when `options` is set to `api`. + */ + 'api' => function ($api = null) { + return $api; + }, + /** + * An array with options + */ + 'options' => function ($options = []) { + return $options; + }, + /** + * Query settings for options queries. This will only take affect when `options` is set to `query`. + */ + 'query' => function ($query = null) { + return $query; + }, + ], + 'computed' => [ + 'options' => function (): array { + return $this->getOptions(); + } + ], + 'methods' => [ + 'getOptions' => function () { + return Options::factory( + $this->options(), + $this->props, + $this->model() + ); + }, + 'sanitizeOption' => function ($option) { + $allowed = array_column($this->options(), 'value'); + return in_array($option, $allowed, true) === true ? $option : null; + }, + 'sanitizeOptions' => function ($options) { + $allowed = array_column($this->options(), 'value'); + return array_intersect($options, $allowed); + }, + ] ]; diff --git a/kirby/config/fields/mixins/pagepicker.php b/kirby/config/fields/mixins/pagepicker.php index bbdc86e..276d8c7 100644 --- a/kirby/config/fields/mixins/pagepicker.php +++ b/kirby/config/fields/mixins/pagepicker.php @@ -3,12 +3,12 @@ use Kirby\Cms\PagePicker; return [ - 'methods' => [ - 'pagepicker' => function (array $params = []) { - // inject the current model - $params['model'] = $this->model(); + 'methods' => [ + 'pagepicker' => function (array $params = []) { + // inject the current model + $params['model'] = $this->model(); - return (new PagePicker($params))->toArray(); - } - ] + return (new PagePicker($params))->toArray(); + } + ] ]; diff --git a/kirby/config/fields/mixins/picker.php b/kirby/config/fields/mixins/picker.php index c2660ad..97b4f8a 100644 --- a/kirby/config/fields/mixins/picker.php +++ b/kirby/config/fields/mixins/picker.php @@ -3,76 +3,76 @@ use Kirby\Toolkit\I18n; return [ - 'props' => [ - /** - * The placeholder text if none have been selected yet - */ - 'empty' => function ($empty = null) { - return I18n::translate($empty, $empty); - }, + 'props' => [ + /** + * The placeholder text if none have been selected yet + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + }, - /** - * Image settings for each item - */ - 'image' => function ($image = null) { - return $image; - }, + /** + * Image settings for each item + */ + 'image' => function ($image = null) { + return $image; + }, - /** - * Info text for each item - */ - 'info' => function (string $info = null) { - return $info; - }, + /** + * Info text for each item + */ + 'info' => function (string $info = null) { + return $info; + }, - /** - * Whether each item should be clickable - */ - 'link' => function (bool $link = true) { - return $link; - }, + /** + * Whether each item should be clickable + */ + 'link' => function (bool $link = true) { + return $link; + }, - /** - * The minimum number of required selected - */ - 'min' => function (int $min = null) { - return $min; - }, + /** + * The minimum number of required selected + */ + 'min' => function (int $min = null) { + return $min; + }, - /** - * The maximum number of allowed selected - */ - 'max' => function (int $max = null) { - return $max; - }, + /** + * The maximum number of allowed selected + */ + 'max' => function (int $max = null) { + return $max; + }, - /** - * If `false`, only a single one can be selected - */ - 'multiple' => function (bool $multiple = true) { - return $multiple; - }, + /** + * If `false`, only a single one can be selected + */ + 'multiple' => function (bool $multiple = true) { + return $multiple; + }, - /** - * Query for the items to be included in the picker - */ - 'query' => function (string $query = null) { - return $query; - }, + /** + * Query for the items to be included in the picker + */ + 'query' => function (string $query = null) { + return $query; + }, - /** - * Enable/disable the search field in the picker - */ - 'search' => function (bool $search = true) { - return $search; - }, + /** + * Enable/disable the search field in the picker + */ + 'search' => function (bool $search = true) { + return $search; + }, - /** - * Main text for each item - */ - 'text' => function (string $text = null) { - return $text; - }, + /** + * Main text for each item + */ + 'text' => function (string $text = null) { + return $text; + }, - ], + ], ]; diff --git a/kirby/config/fields/mixins/upload.php b/kirby/config/fields/mixins/upload.php index 1572ae4..166aeb1 100644 --- a/kirby/config/fields/mixins/upload.php +++ b/kirby/config/fields/mixins/upload.php @@ -5,69 +5,69 @@ use Kirby\Cms\File; use Kirby\Exception\Exception; return [ - 'props' => [ - /** - * Sets the upload options for linked files (since 3.2.0) - */ - 'uploads' => function ($uploads = []) { - if ($uploads === false) { - return false; - } + 'props' => [ + /** + * Sets the upload options for linked files (since 3.2.0) + */ + 'uploads' => function ($uploads = []) { + if ($uploads === false) { + return false; + } - if (is_string($uploads) === true) { - $uploads = ['template' => $uploads]; - } + if (is_string($uploads) === true) { + $uploads = ['template' => $uploads]; + } - if (is_array($uploads) === false) { - $uploads = []; - } + if (is_array($uploads) === false) { + $uploads = []; + } - $template = $uploads['template'] ?? null; + $template = $uploads['template'] ?? null; - if ($template) { - $file = new File([ - 'filename' => 'tmp', - 'parent' => $this->model(), - 'template' => $template - ]); + if ($template) { + $file = new File([ + 'filename' => 'tmp', + 'parent' => $this->model(), + 'template' => $template + ]); - $uploads['accept'] = $file->blueprint()->acceptMime(); - } else { - $uploads['accept'] = '*'; - } + $uploads['accept'] = $file->blueprint()->acceptMime(); + } else { + $uploads['accept'] = '*'; + } - return $uploads; - }, - ], - 'methods' => [ - 'upload' => function (Api $api, $params, Closure $map) { - if ($params === false) { - throw new Exception('Uploads are disabled for this field'); - } + return $uploads; + }, + ], + 'methods' => [ + 'upload' => function (Api $api, $params, Closure $map) { + if ($params === false) { + throw new Exception('Uploads are disabled for this field'); + } - if ($parentQuery = ($params['parent'] ?? null)) { - $parent = $this->model()->query($parentQuery); - } else { - $parent = $this->model(); - } + if ($parentQuery = ($params['parent'] ?? null)) { + $parent = $this->model()->query($parentQuery); + } else { + $parent = $this->model(); + } - if (is_a($parent, 'Kirby\Cms\File') === true) { - $parent = $parent->parent(); - } + if (is_a($parent, 'Kirby\Cms\File') === true) { + $parent = $parent->parent(); + } - return $api->upload(function ($source, $filename) use ($parent, $params, $map) { - $file = $parent->createFile([ - 'source' => $source, - 'template' => $params['template'] ?? null, - 'filename' => $filename, - ]); + return $api->upload(function ($source, $filename) use ($parent, $params, $map) { + $file = $parent->createFile([ + 'source' => $source, + 'template' => $params['template'] ?? null, + 'filename' => $filename, + ]); - if (is_a($file, 'Kirby\Cms\File') === false) { - throw new Exception('The file could not be uploaded'); - } + if (is_a($file, 'Kirby\Cms\File') === false) { + throw new Exception('The file could not be uploaded'); + } - return $map($file, $parent); - }); - } - ] + return $map($file, $parent); + }); + } + ] ]; diff --git a/kirby/config/fields/mixins/userpicker.php b/kirby/config/fields/mixins/userpicker.php index 41c2b62..4f8556c 100644 --- a/kirby/config/fields/mixins/userpicker.php +++ b/kirby/config/fields/mixins/userpicker.php @@ -3,11 +3,11 @@ use Kirby\Cms\UserPicker; return [ - 'methods' => [ - 'userpicker' => function (array $params = []) { - $params['model'] = $this->model(); + 'methods' => [ + 'userpicker' => function (array $params = []) { + $params['model'] = $this->model(); - return (new UserPicker($params))->toArray(); - } - ] + return (new UserPicker($params))->toArray(); + } + ] ]; diff --git a/kirby/config/fields/multiselect.php b/kirby/config/fields/multiselect.php index 4ed2422..37ab356 100644 --- a/kirby/config/fields/multiselect.php +++ b/kirby/config/fields/multiselect.php @@ -1,32 +1,32 @@ 'tags', - 'props' => [ - /** - * Unset inherited props - */ - 'accept' => null, - /** - * Custom icon to replace the arrow down. - */ - 'icon' => function (string $icon = null) { - return $icon; - }, - /** - * Enable/disable the search in the dropdown - * Also limit displayed items (display: 20) - * and set minimum number of characters to search (min: 3) - */ - 'search' => function ($search = true) { - return $search; - }, - /** - * If `true`, selected entries will be sorted - * according to their position in the dropdown - */ - 'sort' => function (bool $sort = false) { - return $sort; - }, - ] + 'extends' => 'tags', + 'props' => [ + /** + * Unset inherited props + */ + 'accept' => null, + /** + * Custom icon to replace the arrow down. + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + /** + * Enable/disable the search in the dropdown + * Also limit displayed items (display: 20) + * and set minimum number of characters to search (min: 3) + */ + 'search' => function ($search = true) { + return $search; + }, + /** + * If `true`, selected entries will be sorted + * according to their position in the dropdown + */ + 'sort' => function (bool $sort = false) { + return $sort; + }, + ] ]; diff --git a/kirby/config/fields/number.php b/kirby/config/fields/number.php index 92a23ee..62470ed 100644 --- a/kirby/config/fields/number.php +++ b/kirby/config/fields/number.php @@ -3,46 +3,46 @@ use Kirby\Toolkit\Str; return [ - 'props' => [ - /** - * Default number that will be saved when a new page/user/file is created - */ - 'default' => function ($default = null) { - return $this->toNumber($default); - }, - /** - * The lowest allowed number - */ - 'min' => function (float $min = null) { - return $min; - }, - /** - * The highest allowed number - */ - 'max' => function (float $max = null) { - return $max; - }, - /** - * Allowed incremental steps between numbers (i.e `0.5`) - */ - 'step' => function ($step = null) { - return $this->toNumber($step); - }, - 'value' => function ($value = null) { - return $this->toNumber($value); - } - ], - 'methods' => [ - 'toNumber' => function ($value) { - if ($this->isEmpty($value) === true) { - return null; - } + 'props' => [ + /** + * Default number that will be saved when a new page/user/file is created + */ + 'default' => function ($default = null) { + return $this->toNumber($default); + }, + /** + * The lowest allowed number + */ + 'min' => function (float $min = null) { + return $min; + }, + /** + * The highest allowed number + */ + 'max' => function (float $max = null) { + return $max; + }, + /** + * Allowed incremental steps between numbers (i.e `0.5`) + */ + 'step' => function ($step = null) { + return $this->toNumber($step); + }, + 'value' => function ($value = null) { + return $this->toNumber($value); + } + ], + 'methods' => [ + 'toNumber' => function ($value) { + if ($this->isEmpty($value) === true) { + return null; + } - return is_float($value) === true ? $value : (float)Str::float($value); - } - ], - 'validations' => [ - 'min', - 'max' - ] + return is_float($value) === true ? $value : (float)Str::float($value); + } + ], + 'validations' => [ + 'min', + 'max' + ] ]; diff --git a/kirby/config/fields/pages.php b/kirby/config/fields/pages.php index 389d75e..8eaa70d 100644 --- a/kirby/config/fields/pages.php +++ b/kirby/config/fields/pages.php @@ -1,110 +1,111 @@ [ - 'layout', - 'min', - 'pagepicker', - 'picker', - ], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'autofocus' => null, - 'before' => null, - 'icon' => null, - 'placeholder' => null, + 'mixins' => [ + 'layout', + 'min', + 'pagepicker', + 'picker', + ], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, - /** - * Default selected page(s) when a new page/file/user is created - */ - 'default' => function ($default = null) { - return $this->toPages($default); - }, + /** + * Default selected page(s) when a new page/file/user is created + */ + 'default' => function ($default = null) { + return $this->toPages($default); + }, - /** - * Optional query to select a specific set of pages - */ - 'query' => function (string $query = null) { - return $query; - }, + /** + * Optional query to select a specific set of pages + */ + 'query' => function (string $query = null) { + return $query; + }, - /** - * Optionally include subpages of pages - */ - 'subpages' => function (bool $subpages = true) { - return $subpages; - }, + /** + * Optionally include subpages of pages + */ + 'subpages' => function (bool $subpages = true) { + return $subpages; + }, - 'value' => function ($value = null) { - return $this->toPages($value); - }, - ], - 'computed' => [ - /** - * Unset inherited computed - */ - 'default' => null - ], - 'methods' => [ - 'pageResponse' => function ($page) { - return $page->panel()->pickerData([ - 'image' => $this->image, - 'info' => $this->info, - 'layout' => $this->layout, - 'text' => $this->text, - ]); - }, - 'toPages' => function ($value = null) { - $pages = []; - $kirby = kirby(); + 'value' => function ($value = null) { + return $this->toPages($value); + }, + ], + 'computed' => [ + /** + * Unset inherited computed + */ + 'default' => null + ], + 'methods' => [ + 'pageResponse' => function ($page) { + return $page->panel()->pickerData([ + 'image' => $this->image, + 'info' => $this->info, + 'layout' => $this->layout, + 'text' => $this->text, + ]); + }, + 'toPages' => function ($value = null) { + $pages = []; + $kirby = App::instance(); - foreach (Data::decode($value, 'yaml') as $id) { - if (is_array($id) === true) { - $id = $id['id'] ?? null; - } + foreach (Data::decode($value, 'yaml') as $id) { + if (is_array($id) === true) { + $id = $id['id'] ?? null; + } - if ($id !== null && ($page = $kirby->page($id))) { - $pages[] = $this->pageResponse($page); - } - } + if ($id !== null && ($page = $kirby->page($id))) { + $pages[] = $this->pageResponse($page); + } + } - return $pages; - } - ], - 'api' => function () { - return [ - [ - 'pattern' => '/', - 'action' => function () { - $field = $this->field(); + return $pages; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); - return $field->pagepicker([ - 'image' => $field->image(), - 'info' => $field->info(), - 'layout' => $field->layout(), - 'limit' => $field->limit(), - 'page' => $this->requestQuery('page'), - 'parent' => $this->requestQuery('parent'), - 'query' => $field->query(), - 'search' => $this->requestQuery('search'), - 'subpages' => $field->subpages(), - 'text' => $field->text() - ]); - } - ] - ]; - }, - 'save' => function ($value = null) { - return A::pluck($value, 'id'); - }, - 'validations' => [ - 'max', - 'min' - ] + return $field->pagepicker([ + 'image' => $field->image(), + 'info' => $field->info(), + 'layout' => $field->layout(), + 'limit' => $field->limit(), + 'page' => $this->requestQuery('page'), + 'parent' => $this->requestQuery('parent'), + 'query' => $field->query(), + 'search' => $this->requestQuery('search'), + 'subpages' => $field->subpages(), + 'text' => $field->text() + ]); + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'id'); + }, + 'validations' => [ + 'max', + 'min' + ] ]; diff --git a/kirby/config/fields/radio.php b/kirby/config/fields/radio.php index dd9ffc3..4846053 100644 --- a/kirby/config/fields/radio.php +++ b/kirby/config/fields/radio.php @@ -1,29 +1,29 @@ ['options'], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'before' => null, - 'icon' => null, - 'placeholder' => null, + 'mixins' => ['options'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, - /** - * Arranges the radio buttons in the given number of columns - */ - 'columns' => function (int $columns = 1) { - return $columns; - }, - ], - 'computed' => [ - 'default' => function () { - return $this->sanitizeOption($this->default); - }, - 'value' => function () { - return $this->sanitizeOption($this->value) ?? ''; - } - ] + /** + * Arranges the radio buttons in the given number of columns + */ + 'columns' => function (int $columns = 1) { + return $columns; + }, + ], + 'computed' => [ + 'default' => function () { + return $this->sanitizeOption($this->default); + }, + 'value' => function () { + return $this->sanitizeOption($this->value) ?? ''; + } + ] ]; diff --git a/kirby/config/fields/range.php b/kirby/config/fields/range.php index 5f14388..04221f1 100644 --- a/kirby/config/fields/range.php +++ b/kirby/config/fields/range.php @@ -1,24 +1,24 @@ 'number', - 'props' => [ - /** - * Unset inherited props - */ - 'placeholder' => null, + 'extends' => 'number', + 'props' => [ + /** + * Unset inherited props + */ + 'placeholder' => null, - /** - * The maximum value on the slider - */ - 'max' => function (float $max = 100) { - return $max; - }, - /** - * Enables/disables the tooltip and set the before and after values - */ - 'tooltip' => function ($tooltip = true) { - return $tooltip; - }, - ] + /** + * The maximum value on the slider + */ + 'max' => function (float $max = 100) { + return $max; + }, + /** + * Enables/disables the tooltip and set the before and after values + */ + 'tooltip' => function ($tooltip = true) { + return $tooltip; + }, + ] ]; diff --git a/kirby/config/fields/select.php b/kirby/config/fields/select.php index 24b14b6..04b468d 100644 --- a/kirby/config/fields/select.php +++ b/kirby/config/fields/select.php @@ -1,24 +1,24 @@ 'radio', - 'props' => [ - /** - * Unset inherited props - */ - 'columns' => null, + 'extends' => 'radio', + 'props' => [ + /** + * Unset inherited props + */ + 'columns' => null, - /** - * Custom icon to replace the arrow down. - */ - 'icon' => function (string $icon = null) { - return $icon; - }, - /** - * Custom placeholder string for empty option. - */ - 'placeholder' => function (string $placeholder = '—') { - return $placeholder; - }, - ] + /** + * Custom icon to replace the arrow down. + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + /** + * Custom placeholder string for empty option. + */ + 'placeholder' => function (string $placeholder = '—') { + return $placeholder; + }, + ] ]; diff --git a/kirby/config/fields/slug.php b/kirby/config/fields/slug.php index d927415..9d8efb5 100644 --- a/kirby/config/fields/slug.php +++ b/kirby/config/fields/slug.php @@ -2,54 +2,54 @@ return [ - 'extends' => 'text', - 'props' => [ - /** - * Unset inherited props - */ - 'converter' => null, - 'counter' => null, - 'spellcheck' => null, + 'extends' => 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'spellcheck' => null, - /** - * Set of characters allowed in the slug - */ - 'allow' => function (string $allow = '') { - return $allow; - }, + /** + * Set of characters allowed in the slug + */ + 'allow' => function (string $allow = '') { + return $allow; + }, - /** - * Changes the link icon - */ - 'icon' => function (string $icon = 'url') { - return $icon; - }, + /** + * Changes the link icon + */ + 'icon' => function (string $icon = 'url') { + return $icon; + }, - /** - * Set prefix for the help text - */ - 'path' => function (string $path = null) { - return $path; - }, + /** + * Set prefix for the help text + */ + 'path' => function (string $path = null) { + return $path; + }, - /** - * Name of another field that should be used to - * automatically update this field's value - */ - 'sync' => function (string $sync = null) { - return $sync; - }, + /** + * Name of another field that should be used to + * automatically update this field's value + */ + 'sync' => function (string $sync = null) { + return $sync; + }, - /** - * Set to object with keys `field` and `text` to add - * button to generate from another field - */ - 'wizard' => function ($wizard = false) { - return $wizard; - } - ], - 'validations' => [ - 'minlength', - 'maxlength' - ], + /** + * Set to object with keys `field` and `text` to add + * button to generate from another field + */ + 'wizard' => function ($wizard = false) { + return $wizard; + } + ], + 'validations' => [ + 'minlength', + 'maxlength' + ], ]; diff --git a/kirby/config/fields/structure.php b/kirby/config/fields/structure.php index 240406f..ee74703 100644 --- a/kirby/config/fields/structure.php +++ b/kirby/config/fields/structure.php @@ -5,189 +5,199 @@ use Kirby\Form\Form; use Kirby\Toolkit\I18n; return [ - 'mixins' => ['min'], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'before' => null, - 'autofocus' => null, - 'icon' => null, - 'placeholder' => null, + 'mixins' => ['min'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'autofocus' => null, + 'icon' => null, + 'placeholder' => null, - /** - * Optional columns definition to only show selected fields in the structure table. - */ - 'columns' => function (array $columns = []) { - // lower case all keys, because field names will - // be lowercase as well. - return array_change_key_case($columns); - }, + /** + * Optional columns definition to only show selected fields in the structure table. + */ + 'columns' => function (array $columns = []) { + // lower case all keys, because field names will + // be lowercase as well. + return array_change_key_case($columns); + }, - /** - * Toggles duplicating rows for the structure - */ - 'duplicate' => function (bool $duplicate = true) { - return $duplicate; - }, + /** + * Toggles duplicating rows for the structure + */ + 'duplicate' => function (bool $duplicate = true) { + return $duplicate; + }, - /** - * The placeholder text if no items have been added yet - */ - 'empty' => function ($empty = null) { - return I18n::translate($empty, $empty); - }, + /** + * The placeholder text if no items have been added yet + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + }, - /** - * Set the default rows for the structure - */ - 'default' => function (array $default = null) { - return $default; - }, + /** + * Set the default rows for the structure + */ + 'default' => function (array $default = null) { + return $default; + }, - /** - * Fields setup for the structure form. Works just like fields in regular forms. - */ - 'fields' => function (array $fields) { - return $fields; - }, - /** - * The number of entries that will be displayed on a single page. Afterwards pagination kicks in. - */ - 'limit' => function (int $limit = null) { - return $limit; - }, - /** - * Maximum allowed entries in the structure. Afterwards the "Add" button will be switched off. - */ - 'max' => function (int $max = null) { - return $max; - }, - /** - * Minimum required entries in the structure - */ - 'min' => function (int $min = null) { - return $min; - }, - /** - * Toggles adding to the top or bottom of the list - */ - 'prepend' => function (bool $prepend = null) { - return $prepend; - }, - /** - * Toggles drag & drop sorting - */ - 'sortable' => function (bool $sortable = null) { - return $sortable; - }, - /** - * Sorts the entries by the given field and order (i.e. `title desc`) - * Drag & drop is disabled in this case - */ - 'sortBy' => function (string $sort = null) { - return $sort; - } - ], - 'computed' => [ - 'default' => function () { - return $this->rows($this->default); - }, - 'value' => function () { - return $this->rows($this->value); - }, - 'fields' => function () { - if (empty($this->fields) === true) { - throw new Exception('Please provide some fields for the structure'); - } + /** + * Fields setup for the structure form. Works just like fields in regular forms. + */ + 'fields' => function (array $fields) { + return $fields; + }, + /** + * The number of entries that will be displayed on a single page. Afterwards pagination kicks in. + */ + 'limit' => function (int $limit = null) { + return $limit; + }, + /** + * Maximum allowed entries in the structure. Afterwards the "Add" button will be switched off. + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * Minimum required entries in the structure + */ + 'min' => function (int $min = null) { + return $min; + }, + /** + * Toggles adding to the top or bottom of the list + */ + 'prepend' => function (bool $prepend = null) { + return $prepend; + }, + /** + * Toggles drag & drop sorting + */ + 'sortable' => function (bool $sortable = null) { + return $sortable; + }, + /** + * Sorts the entries by the given field and order (i.e. `title desc`) + * Drag & drop is disabled in this case + */ + 'sortBy' => function (string $sort = null) { + return $sort; + } + ], + 'computed' => [ + 'default' => function () { + return $this->rows($this->default); + }, + 'value' => function () { + return $this->rows($this->value); + }, + 'fields' => function () { + if (empty($this->fields) === true) { + throw new Exception('Please provide some fields for the structure'); + } - return $this->form()->fields()->toArray(); - }, - 'columns' => function () { - $columns = []; + return $this->form()->fields()->toArray(); + }, + 'columns' => function () { + $columns = []; + $mobile = 0; - if (empty($this->columns)) { - foreach ($this->fields as $field) { + if (empty($this->columns)) { + foreach ($this->fields as $field) { + // Skip hidden and unsaveable fields + // They should never be included as column + if ($field['type'] === 'hidden' || $field['saveable'] === false) { + continue; + } - // Skip hidden and unsaveable fields - // They should never be included as column - if ($field['type'] === 'hidden' || $field['saveable'] === false) { - continue; - } + $columns[$field['name']] = [ + 'type' => $field['type'], + 'label' => $field['label'] ?? $field['name'] + ]; + } + } else { + foreach ($this->columns as $columnName => $columnProps) { + if (is_array($columnProps) === false) { + $columnProps = []; + } - $columns[$field['name']] = [ - 'type' => $field['type'], - 'label' => $field['label'] ?? $field['name'] - ]; - } - } else { - foreach ($this->columns as $columnName => $columnProps) { - if (is_array($columnProps) === false) { - $columnProps = []; - } + $field = $this->fields[$columnName] ?? null; - $field = $this->fields[$columnName] ?? null; + if (empty($field) === true || $field['saveable'] === false) { + continue; + } - if (empty($field) === true || $field['saveable'] === false) { - continue; - } + if (($columnProps['mobile'] ?? false) === true) { + $mobile++; + } - $columns[$columnName] = array_merge($columnProps, [ - 'type' => $field['type'], - 'label' => $field['label'] ?? $field['name'] - ]); - } - } + $columns[$columnName] = array_merge($columnProps, [ + 'type' => $field['type'], + 'label' => $field['label'] ?? $field['name'] + ]); + } + } - return $columns; - } - ], - 'methods' => [ - 'rows' => function ($value) { - $rows = Data::decode($value, 'yaml'); - $value = []; + // make the first column visible on mobile + // if no other mobile columns are defined + if ($mobile === 0) { + $columns[array_key_first($columns)]['mobile'] = true; + } - foreach ($rows as $index => $row) { - if (is_array($row) === false) { - continue; - } + return $columns; + } + ], + 'methods' => [ + 'rows' => function ($value) { + $rows = Data::decode($value, 'yaml'); + $value = []; - $value[] = $this->form($row)->values(); - } + foreach ($rows as $index => $row) { + if (is_array($row) === false) { + continue; + } - return $value; - }, - 'form' => function (array $values = []) { - return new Form([ - 'fields' => $this->attrs['fields'], - 'values' => $values, - 'model' => $this->model - ]); - }, - ], - 'api' => function () { - return [ - [ - 'pattern' => 'validate', - 'method' => 'ALL', - 'action' => function () { - return array_values($this->field()->form($this->requestBody())->errors()); - } - ] - ]; - }, - 'save' => function ($value) { - $data = []; + $value[] = $this->form($row)->values(); + } - foreach ($value as $row) { - $data[] = $this->form($row)->content(); - } + return $value; + }, + 'form' => function (array $values = []) { + return new Form([ + 'fields' => $this->attrs['fields'], + 'values' => $values, + 'model' => $this->model + ]); + }, + ], + 'api' => function () { + return [ + [ + 'pattern' => 'validate', + 'method' => 'ALL', + 'action' => function () { + return array_values($this->field()->form($this->requestBody())->errors()); + } + ] + ]; + }, + 'save' => function ($value) { + $data = []; - return $data; - }, - 'validations' => [ - 'min', - 'max' - ] + foreach ($value as $row) { + $data[] = $this->form($row)->content(); + } + + return $data; + }, + 'validations' => [ + 'min', + 'max' + ] ]; diff --git a/kirby/config/fields/tags.php b/kirby/config/fields/tags.php index 5cfd4f4..ab8121a 100644 --- a/kirby/config/fields/tags.php +++ b/kirby/config/fields/tags.php @@ -5,99 +5,98 @@ use Kirby\Toolkit\Str; use Kirby\Toolkit\V; return [ - 'mixins' => ['min', 'options'], - 'props' => [ + 'mixins' => ['min', 'options'], + 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'before' => null, - 'placeholder' => null, + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'placeholder' => null, - /** - * If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input. - */ - 'accept' => function ($value = 'all') { - return V::in($value, ['all', 'options']) ? $value : 'all'; - }, - /** - * Changes the tag icon - */ - 'icon' => function ($icon = 'tag') { - return $icon; - }, - /** - * Set to `list` to display each tag with 100% width, - * otherwise the tags are displayed inline - */ - 'layout' => function (?string $layout = null) { - return $layout; - }, - /** - * Minimum number of required entries/tags - */ - 'min' => function (int $min = null) { - return $min; - }, - /** - * Maximum number of allowed entries/tags - */ - 'max' => function (int $max = null) { - return $max; - }, - /** - * Custom tags separator, which will be used to store tags in the content file - */ - 'separator' => function (string $separator = ',') { - return $separator; - }, - ], - 'computed' => [ - 'default' => function (): array { - return $this->toTags($this->default); - }, - 'value' => function (): array { - return $this->toTags($this->value); - } - ], - 'methods' => [ - 'toTags' => function ($value) { - if (is_null($value) === true) { - return []; - } + /** + * If set to `all`, any type of input is accepted. If set to `options` only the predefined options are accepted as input. + */ + 'accept' => function ($value = 'all') { + return V::in($value, ['all', 'options']) ? $value : 'all'; + }, + /** + * Changes the tag icon + */ + 'icon' => function ($icon = 'tag') { + return $icon; + }, + /** + * Set to `list` to display each tag with 100% width, + * otherwise the tags are displayed inline + */ + 'layout' => function (?string $layout = null) { + return $layout; + }, + /** + * Minimum number of required entries/tags + */ + 'min' => function (int $min = null) { + return $min; + }, + /** + * Maximum number of allowed entries/tags + */ + 'max' => function (int $max = null) { + return $max; + }, + /** + * Custom tags separator, which will be used to store tags in the content file + */ + 'separator' => function (string $separator = ',') { + return $separator; + }, + ], + 'computed' => [ + 'default' => function (): array { + return $this->toTags($this->default); + }, + 'value' => function (): array { + return $this->toTags($this->value); + } + ], + 'methods' => [ + 'toTags' => function ($value) { + if (is_null($value) === true) { + return []; + } - $options = $this->options(); + $options = $this->options(); - // transform into value-text objects - return array_map(function ($option) use ($options) { + // transform into value-text objects + return array_map(function ($option) use ($options) { + // already a valid object + if (is_array($option) === true && isset($option['value'], $option['text']) === true) { + return $option; + } - // already a valid object - if (is_array($option) === true && isset($option['value'], $option['text']) === true) { - return $option; - } + $index = array_search($option, array_column($options, 'value')); - $index = array_search($option, array_column($options, 'value')); + if ($index !== false) { + return $options[$index]; + } - if ($index !== false) { - return $options[$index]; - } - - return [ - 'value' => $option, - 'text' => $option, - ]; - }, Str::split($value, $this->separator())); - } - ], - 'save' => function (array $value = null): string { - return A::join( - A::pluck($value, 'value'), - $this->separator() . ' ' - ); - }, - 'validations' => [ - 'min', - 'max' - ] + return [ + 'value' => $option, + 'text' => $option, + ]; + }, Str::split($value, $this->separator())); + } + ], + 'save' => function (array $value = null): string { + return A::join( + A::pluck($value, 'value'), + $this->separator() . ' ' + ); + }, + 'validations' => [ + 'min', + 'max' + ] ]; diff --git a/kirby/config/fields/tel.php b/kirby/config/fields/tel.php index 3d73430..715d587 100644 --- a/kirby/config/fields/tel.php +++ b/kirby/config/fields/tel.php @@ -1,27 +1,27 @@ 'text', - 'props' => [ - /** - * Unset inherited props - */ - 'converter' => null, - 'counter' => null, - 'spellcheck' => null, + 'extends' => 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'spellcheck' => null, - /** - * Sets the HTML5 autocomplete attribute - */ - 'autocomplete' => function (string $autocomplete = 'tel') { - return $autocomplete; - }, + /** + * Sets the HTML5 autocomplete attribute + */ + 'autocomplete' => function (string $autocomplete = 'tel') { + return $autocomplete; + }, - /** - * Changes the phone icon - */ - 'icon' => function (string $icon = 'phone') { - return $icon; - } - ] + /** + * Changes the phone icon + */ + 'icon' => function (string $icon = 'phone') { + return $icon; + } + ] ]; diff --git a/kirby/config/fields/text.php b/kirby/config/fields/text.php index c32a037..7abd86f 100644 --- a/kirby/config/fields/text.php +++ b/kirby/config/fields/text.php @@ -4,100 +4,99 @@ use Kirby\Exception\InvalidArgumentException; use Kirby\Toolkit\Str; return [ - 'props' => [ + 'props' => [ - /** - * The field value will be converted with the selected converter before the value gets saved. Available converters: `lower`, `upper`, `ucfirst`, `slug` - */ - 'converter' => function ($value = null) { - if ($value !== null && in_array($value, array_keys($this->converters())) === false) { - throw new InvalidArgumentException([ - 'key' => 'field.converter.invalid', - 'data' => ['converter' => $value] - ]); - } + /** + * The field value will be converted with the selected converter before the value gets saved. Available converters: `lower`, `upper`, `ucfirst`, `slug` + */ + 'converter' => function ($value = null) { + if ($value !== null && in_array($value, array_keys($this->converters())) === false) { + throw new InvalidArgumentException([ + 'key' => 'field.converter.invalid', + 'data' => ['converter' => $value] + ]); + } - return $value; - }, + return $value; + }, - /** - * Shows or hides the character counter in the top right corner - */ - 'counter' => function (bool $counter = true) { - return $counter; - }, + /** + * Shows or hides the character counter in the top right corner + */ + 'counter' => function (bool $counter = true) { + return $counter; + }, - /** - * Maximum number of allowed characters - */ - 'maxlength' => function (int $maxlength = null) { - return $maxlength; - }, + /** + * Maximum number of allowed characters + */ + 'maxlength' => function (int $maxlength = null) { + return $maxlength; + }, - /** - * Minimum number of required characters - */ - 'minlength' => function (int $minlength = null) { - return $minlength; - }, + /** + * Minimum number of required characters + */ + 'minlength' => function (int $minlength = null) { + return $minlength; + }, - /** - * A regular expression, which will be used to validate the input - */ - 'pattern' => function (string $pattern = null) { - return $pattern; - }, + /** + * A regular expression, which will be used to validate the input + */ + 'pattern' => function (string $pattern = null) { + return $pattern; + }, - /** - * If `false`, spellcheck will be switched off - */ - 'spellcheck' => function (bool $spellcheck = false) { - return $spellcheck; - }, - ], - 'computed' => [ - 'default' => function () { - return $this->convert($this->default); - }, - 'value' => function () { - return (string)$this->convert($this->value); - } - ], - 'methods' => [ - 'convert' => function ($value) { - if ($this->converter() === null) { - return $value; - } + /** + * If `false`, spellcheck will be switched off + */ + 'spellcheck' => function (bool $spellcheck = false) { + return $spellcheck; + }, + ], + 'computed' => [ + 'default' => function () { + return $this->convert($this->default); + }, + 'value' => function () { + return (string)$this->convert($this->value); + } + ], + 'methods' => [ + 'convert' => function ($value) { + if ($this->converter() === null) { + return $value; + } - $value = trim($value); - $converter = $this->converters()[$this->converter()]; + $converter = $this->converters()[$this->converter()]; - if (is_array($value) === true) { - return array_map($converter, $value); - } + if (is_array($value) === true) { + return array_map($converter, $value); + } - return call_user_func($converter, $value); - }, - 'converters' => function (): array { - return [ - 'lower' => function ($value) { - return Str::lower($value); - }, - 'slug' => function ($value) { - return Str::slug($value); - }, - 'ucfirst' => function ($value) { - return Str::ucfirst($value); - }, - 'upper' => function ($value) { - return Str::upper($value); - }, - ]; - }, - ], - 'validations' => [ - 'minlength', - 'maxlength', - 'pattern' - ] + return call_user_func($converter, trim($value ?? '')); + }, + 'converters' => function (): array { + return [ + 'lower' => function ($value) { + return Str::lower($value); + }, + 'slug' => function ($value) { + return Str::slug($value); + }, + 'ucfirst' => function ($value) { + return Str::ucfirst($value); + }, + 'upper' => function ($value) { + return Str::upper($value); + }, + ]; + }, + ], + 'validations' => [ + 'minlength', + 'maxlength', + 'pattern' + ] ]; diff --git a/kirby/config/fields/textarea.php b/kirby/config/fields/textarea.php index aaf2962..7b51c1f 100644 --- a/kirby/config/fields/textarea.php +++ b/kirby/config/fields/textarea.php @@ -1,123 +1,123 @@ ['filepicker', 'upload'], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'before' => null, + 'mixins' => ['filepicker', 'upload'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, - /** - * Enables/disables the format buttons. Can either be `true`/`false` or a list of allowed buttons. Available buttons: `headlines`, `italic`, `bold`, `link`, `email`, `file`, `code`, `ul`, `ol` (as well as `|` for a divider) - */ - 'buttons' => function ($buttons = true) { - return $buttons; - }, + /** + * Enables/disables the format buttons. Can either be `true`/`false` or a list of allowed buttons. Available buttons: `headlines`, `italic`, `bold`, `link`, `email`, `file`, `code`, `ul`, `ol` (as well as `|` for a divider) + */ + 'buttons' => function ($buttons = true) { + return $buttons; + }, - /** - * Enables/disables the character counter in the top right corner - */ - 'counter' => function (bool $counter = true) { - return $counter; - }, + /** + * Enables/disables the character counter in the top right corner + */ + 'counter' => function (bool $counter = true) { + return $counter; + }, - /** - * Sets the default text when a new page/file/user is created - */ - 'default' => function (string $default = null) { - return trim($default ?? ''); - }, + /** + * Sets the default text when a new page/file/user is created + */ + 'default' => function (string $default = null) { + return trim($default ?? ''); + }, - /** - * Sets the options for the files picker - */ - 'files' => function ($files = []) { - if (is_string($files) === true) { - return ['query' => $files]; - } + /** + * Sets the options for the files picker + */ + 'files' => function ($files = []) { + if (is_string($files) === true) { + return ['query' => $files]; + } - if (is_array($files) === false) { - $files = []; - } + if (is_array($files) === false) { + $files = []; + } - return $files; - }, + return $files; + }, - /** - * Sets the font family (sans or monospace) - */ - 'font' => function (string $font = null) { - return $font === 'monospace' ? 'monospace' : 'sans-serif'; - }, + /** + * Sets the font family (sans or monospace) + */ + 'font' => function (string $font = null) { + return $font === 'monospace' ? 'monospace' : 'sans-serif'; + }, - /** - * Maximum number of allowed characters - */ - 'maxlength' => function (int $maxlength = null) { - return $maxlength; - }, + /** + * Maximum number of allowed characters + */ + 'maxlength' => function (int $maxlength = null) { + return $maxlength; + }, - /** - * Minimum number of required characters - */ - 'minlength' => function (int $minlength = null) { - return $minlength; - }, + /** + * Minimum number of required characters + */ + 'minlength' => function (int $minlength = null) { + return $minlength; + }, - /** - * Changes the size of the textarea. Available sizes: `small`, `medium`, `large`, `huge` - */ - 'size' => function (string $size = null) { - return $size; - }, + /** + * Changes the size of the textarea. Available sizes: `small`, `medium`, `large`, `huge` + */ + 'size' => function (string $size = null) { + return $size; + }, - /** - * If `false`, spellcheck will be switched off - */ - 'spellcheck' => function (bool $spellcheck = true) { - return $spellcheck; - }, + /** + * If `false`, spellcheck will be switched off + */ + 'spellcheck' => function (bool $spellcheck = true) { + return $spellcheck; + }, - 'value' => function (string $value = null) { - return trim($value ?? ''); - } - ], - 'api' => function () { - return [ - [ - 'pattern' => 'files', - 'action' => function () { - $params = array_merge($this->field()->files(), [ - 'page' => $this->requestQuery('page'), - 'search' => $this->requestQuery('search') - ]); + 'value' => function (string $value = null) { + return trim($value ?? ''); + } + ], + 'api' => function () { + return [ + [ + 'pattern' => 'files', + 'action' => function () { + $params = array_merge($this->field()->files(), [ + 'page' => $this->requestQuery('page'), + 'search' => $this->requestQuery('search') + ]); - return $this->field()->filepicker($params); - } - ], - [ - 'pattern' => 'upload', - 'method' => 'POST', - 'action' => function () { - $field = $this->field(); - $uploads = $field->uploads(); + return $this->field()->filepicker($params); + } + ], + [ + 'pattern' => 'upload', + 'method' => 'POST', + 'action' => function () { + $field = $this->field(); + $uploads = $field->uploads(); - return $this->field()->upload($this, $uploads, function ($file, $parent) use ($field) { - $absolute = $field->model()->is($parent) === false; + return $this->field()->upload($this, $uploads, function ($file, $parent) use ($field) { + $absolute = $field->model()->is($parent) === false; - return [ - 'filename' => $file->filename(), - 'dragText' => $file->panel()->dragText('auto', $absolute), - ]; - }); - } - ] - ]; - }, - 'validations' => [ - 'minlength', - 'maxlength' - ] + return [ + 'filename' => $file->filename(), + 'dragText' => $file->panel()->dragText('auto', $absolute), + ]; + }); + } + ] + ]; + }, + 'validations' => [ + 'minlength', + 'maxlength' + ] ]; diff --git a/kirby/config/fields/time.php b/kirby/config/fields/time.php index 5dbb536..69a2da9 100644 --- a/kirby/config/fields/time.php +++ b/kirby/config/fields/time.php @@ -5,122 +5,122 @@ use Kirby\Toolkit\Date; use Kirby\Toolkit\I18n; return [ - 'mixins' => ['datetime'], - 'props' => [ - /** - * Unset inherited props - */ - 'placeholder' => null, + 'mixins' => ['datetime'], + 'props' => [ + /** + * Unset inherited props + */ + 'placeholder' => null, - /** - * Sets the default time when a new page/file/user is created - */ - 'default' => function ($default = null): ?string { - return $default; - }, + /** + * Sets the default time when a new page/file/user is created + */ + 'default' => function ($default = null): ?string { + return $default; + }, - /** - * Custom format (dayjs tokens: `HH`, `hh`, `mm`, `ss`, `a`) that is - * used to display the field in the Panel - */ - 'display' => function ($display = null) { - return I18n::translate($display, $display); - }, + /** + * Custom format (dayjs tokens: `HH`, `hh`, `mm`, `ss`, `a`) that is + * used to display the field in the Panel + */ + 'display' => function ($display = null) { + return I18n::translate($display, $display); + }, - /** - * Changes the clock icon - */ - 'icon' => function (string $icon = 'clock') { - return $icon; - }, - /** - * Latest time, which can be selected/saved (H:i or H:i:s) - */ - 'max' => function (string $max = null): ?string { - return Date::optional($max); - }, - /** - * Earliest time, which can be selected/saved (H:i or H:i:s) - */ - 'min' => function (string $min = null): ?string { - return Date::optional($min); - }, + /** + * Changes the clock icon + */ + 'icon' => function (string $icon = 'clock') { + return $icon; + }, + /** + * Latest time, which can be selected/saved (H:i or H:i:s) + */ + 'max' => function (string $max = null): ?string { + return Date::optional($max); + }, + /** + * Earliest time, which can be selected/saved (H:i or H:i:s) + */ + 'min' => function (string $min = null): ?string { + return Date::optional($min); + }, - /** - * `12` or `24` hour notation. If `12`, an AM/PM selector will be shown. - * If `display` is defined, that option will take priority. - */ - 'notation' => function (int $value = 24) { - return $value === 24 ? 24 : 12; - }, - /** - * Round to the nearest: sub-options for `unit` (minute) and `size` (5) - */ - 'step' => function ($step = null) { - return Date::stepConfig($step, [ - 'size' => 5, - 'unit' => 'minute', - ]); - }, - 'value' => function ($value = null): ?string { - return $value; - } - ], - 'computed' => [ - 'display' => function () { - if ($this->display) { - return $this->display; - } + /** + * `12` or `24` hour notation. If `12`, an AM/PM selector will be shown. + * If `display` is defined, that option will take priority. + */ + 'notation' => function (int $value = 24) { + return $value === 24 ? 24 : 12; + }, + /** + * Round to the nearest: sub-options for `unit` (minute) and `size` (5) + */ + 'step' => function ($step = null) { + return Date::stepConfig($step, [ + 'size' => 5, + 'unit' => 'minute', + ]); + }, + 'value' => function ($value = null): ?string { + return $value; + } + ], + 'computed' => [ + 'display' => function () { + if ($this->display) { + return $this->display; + } - return $this->notation === 24 ? 'HH:mm' : 'hh:mm a'; - }, - 'default' => function (): string { - return $this->toDatetime($this->default, 'H:i:s') ?? ''; - }, - 'format' => function () { - return $this->props['format'] ?? 'H:i:s'; - }, - 'value' => function (): ?string { - return $this->toDatetime($this->value, 'H:i:s') ?? ''; - } - ], - 'validations' => [ - 'time', - 'minMax' => function ($value) { - if (!$value = Date::optional($value)) { - return true; - } + return $this->notation === 24 ? 'HH:mm' : 'hh:mm a'; + }, + 'default' => function (): string { + return $this->toDatetime($this->default, 'H:i:s') ?? ''; + }, + 'format' => function () { + return $this->props['format'] ?? 'H:i:s'; + }, + 'value' => function (): ?string { + return $this->toDatetime($this->value, 'H:i:s') ?? ''; + } + ], + 'validations' => [ + 'time', + 'minMax' => function ($value) { + if (!$value = Date::optional($value)) { + return true; + } - $min = Date::optional($this->min); - $max = Date::optional($this->max); + $min = Date::optional($this->min); + $max = Date::optional($this->max); - $format = 'H:i:s'; + $format = 'H:i:s'; - if ($min && $max && $value->isBetween($min, $max) === false) { - throw new Exception([ - 'key' => 'validation.time.between', - 'data' => [ - 'min' => $min->format($format), - 'max' => $min->format($format) - ] - ]); - } elseif ($min && $value->isMin($min) === false) { - throw new Exception([ - 'key' => 'validation.time.after', - 'data' => [ - 'time' => $min->format($format), - ] - ]); - } elseif ($max && $value->isMax($max) === false) { - throw new Exception([ - 'key' => 'validation.time.before', - 'data' => [ - 'time' => $max->format($format), - ] - ]); - } + if ($min && $max && $value->isBetween($min, $max) === false) { + throw new Exception([ + 'key' => 'validation.time.between', + 'data' => [ + 'min' => $min->format($format), + 'max' => $min->format($format) + ] + ]); + } elseif ($min && $value->isMin($min) === false) { + throw new Exception([ + 'key' => 'validation.time.after', + 'data' => [ + 'time' => $min->format($format), + ] + ]); + } elseif ($max && $value->isMax($max) === false) { + throw new Exception([ + 'key' => 'validation.time.before', + 'data' => [ + 'time' => $max->format($format), + ] + ]); + } - return true; - }, - ] + return true; + }, + ] ]; diff --git a/kirby/config/fields/toggle.php b/kirby/config/fields/toggle.php index 6ea330f..4cb8a6e 100644 --- a/kirby/config/fields/toggle.php +++ b/kirby/config/fields/toggle.php @@ -5,69 +5,69 @@ use Kirby\Toolkit\A; use Kirby\Toolkit\I18n; return [ - 'props' => [ - /** - * Unset inherited props - */ - 'placeholder' => null, + 'props' => [ + /** + * Unset inherited props + */ + 'placeholder' => null, - /** - * Default value which will be saved when a new page/user/file is created - */ - 'default' => function ($default = null) { - return $this->default = $default; - }, - /** - * Sets the text next to the toggle. The text can be a string or an array of two options. The first one is the negative text and the second one the positive. The text will automatically switch when the toggle is triggered. - */ - 'text' => function ($value = null) { - $model = $this->model(); + /** + * Default value which will be saved when a new page/user/file is created + */ + 'default' => function ($default = null) { + return $this->default = $default; + }, + /** + * Sets the text next to the toggle. The text can be a string or an array of two options. The first one is the negative text and the second one the positive. The text will automatically switch when the toggle is triggered. + */ + 'text' => function ($value = null) { + $model = $this->model(); - if (is_array($value) === true) { - if (A::isAssociative($value) === true) { - return $model->toSafeString(I18n::translate($value, $value)); - } + if (is_array($value) === true) { + if (A::isAssociative($value) === true) { + return $model->toSafeString(I18n::translate($value, $value)); + } - foreach ($value as $key => $val) { - $value[$key] = $model->toSafeString(I18n::translate($val, $val)); - } + foreach ($value as $key => $val) { + $value[$key] = $model->toSafeString(I18n::translate($val, $val)); + } - return $value; - } + return $value; + } - if (empty($value) === false) { - return $model->toSafeString(I18n::translate($value, $value)); - } + if (empty($value) === false) { + return $model->toSafeString(I18n::translate($value, $value)); + } - return $value; - }, - ], - 'computed' => [ - 'default' => function () { - return $this->toBool($this->default); - }, - 'value' => function () { - if ($this->props['value'] === null) { - return $this->default(); - } else { - return $this->toBool($this->props['value']); - } - } - ], - 'methods' => [ - 'toBool' => function ($value) { - return in_array($value, [true, 'true', 1, '1', 'on'], true) === true; - } - ], - 'save' => function (): string { - return $this->value() === true ? 'true' : 'false'; - }, - 'validations' => [ - 'boolean', - 'required' => function ($value) { - if ($this->isRequired() && ($value === false || $this->isEmpty($value))) { - throw new InvalidArgumentException(I18n::translate('field.required')); - } - }, - ] + return $value; + }, + ], + 'computed' => [ + 'default' => function () { + return $this->toBool($this->default); + }, + 'value' => function () { + if ($this->props['value'] === null) { + return $this->default(); + } else { + return $this->toBool($this->props['value']); + } + } + ], + 'methods' => [ + 'toBool' => function ($value) { + return in_array($value, [true, 'true', 1, '1', 'on'], true) === true; + } + ], + 'save' => function (): string { + return $this->value() === true ? 'true' : 'false'; + }, + 'validations' => [ + 'boolean', + 'required' => function ($value) { + if ($this->isRequired() && ($value === false || $this->isEmpty($value))) { + throw new InvalidArgumentException(I18n::translate('field.required')); + } + }, + ] ]; diff --git a/kirby/config/fields/toggles.php b/kirby/config/fields/toggles.php new file mode 100644 index 0000000..c922c2b --- /dev/null +++ b/kirby/config/fields/toggles.php @@ -0,0 +1,41 @@ + ['options'], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, + + /** + * Toggles will automatically span the full width of the field. With the grow option, you can disable this behaviour for a more compact layout. + */ + 'grow' => function (bool $grow = true) { + return $grow; + }, + /** + * If `false` all labels will be hidden for icon-only toggles. + */ + 'labels' => function (bool $labels = true) { + return $labels; + }, + /** + * A toggle can be deactivated on click. If reset is `false` deactivating a toggle is no longer possible. + */ + 'reset' => function (bool $reset = true) { + return $reset; + } + ], + 'computed' => [ + 'default' => function () { + return $this->sanitizeOption($this->default); + }, + 'value' => function () { + return $this->sanitizeOption($this->value) ?? ''; + }, + ] +]; diff --git a/kirby/config/fields/url.php b/kirby/config/fields/url.php index f92dd2c..1ecab71 100644 --- a/kirby/config/fields/url.php +++ b/kirby/config/fields/url.php @@ -3,39 +3,40 @@ use Kirby\Toolkit\I18n; return [ - 'extends' => 'text', - 'props' => [ - /** - * Unset inherited props - */ - 'converter' => null, - 'counter' => null, - 'spellcheck' => null, + 'extends' => 'text', + 'props' => [ + /** + * Unset inherited props + */ + 'converter' => null, + 'counter' => null, + 'pattern' => null, + 'spellcheck' => null, - /** - * Sets the HTML5 autocomplete attribute - */ - 'autocomplete' => function (string $autocomplete = 'url') { - return $autocomplete; - }, + /** + * Sets the HTML5 autocomplete attribute + */ + 'autocomplete' => function (string $autocomplete = 'url') { + return $autocomplete; + }, - /** - * Changes the link icon - */ - 'icon' => function (string $icon = 'url') { - return $icon; - }, + /** + * Changes the link icon + */ + 'icon' => function (string $icon = 'url') { + return $icon; + }, - /** - * Sets custom placeholder text, when the field is empty - */ - 'placeholder' => function ($value = null) { - return I18n::translate($value, $value) ?? 'https://example.com'; - } - ], - 'validations' => [ - 'minlength', - 'maxlength', - 'url' - ], + /** + * Sets custom placeholder text, when the field is empty + */ + 'placeholder' => function ($value = null) { + return I18n::translate($value, $value) ?? 'https://example.com'; + } + ], + 'validations' => [ + 'minlength', + 'maxlength', + 'url' + ], ]; diff --git a/kirby/config/fields/users.php b/kirby/config/fields/users.php index 9608c9e..8641eee 100644 --- a/kirby/config/fields/users.php +++ b/kirby/config/fields/users.php @@ -1,104 +1,105 @@ [ - 'layout', - 'min', - 'picker', - 'userpicker' - ], - 'props' => [ - /** - * Unset inherited props - */ - 'after' => null, - 'autofocus' => null, - 'before' => null, - 'icon' => null, - 'placeholder' => null, + 'mixins' => [ + 'layout', + 'min', + 'picker', + 'userpicker' + ], + 'props' => [ + /** + * Unset inherited props + */ + 'after' => null, + 'autofocus' => null, + 'before' => null, + 'icon' => null, + 'placeholder' => null, - /** - * Default selected user(s) when a new page/file/user is created - */ - 'default' => function ($default = null) { - if ($default === false) { - return []; - } + /** + * Default selected user(s) when a new page/file/user is created + */ + 'default' => function ($default = null) { + if ($default === false) { + return []; + } - if ($default === null && $user = $this->kirby()->user()) { - return [ - $this->userResponse($user) - ]; - } + if ($default === null && $user = $this->kirby()->user()) { + return [ + $this->userResponse($user) + ]; + } - return $this->toUsers($default); - }, + return $this->toUsers($default); + }, - 'value' => function ($value = null) { - return $this->toUsers($value); - }, - ], - 'computed' => [ - /** - * Unset inherited computed - */ - 'default' => null - ], - 'methods' => [ - 'userResponse' => function ($user) { - return $user->panel()->pickerData([ - 'info' => $this->info, - 'image' => $this->image, - 'layout' => $this->layout, - 'text' => $this->text, - ]); - }, - 'toUsers' => function ($value = null) { - $users = []; - $kirby = kirby(); + 'value' => function ($value = null) { + return $this->toUsers($value); + }, + ], + 'computed' => [ + /** + * Unset inherited computed + */ + 'default' => null + ], + 'methods' => [ + 'userResponse' => function ($user) { + return $user->panel()->pickerData([ + 'info' => $this->info, + 'image' => $this->image, + 'layout' => $this->layout, + 'text' => $this->text, + ]); + }, + 'toUsers' => function ($value = null) { + $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 $email) { + if (is_array($email) === true) { + $email = $email['email'] ?? null; + } - if ($email !== null && ($user = $kirby->user($email))) { - $users[] = $this->userResponse($user); - } - } + if ($email !== null && ($user = $kirby->user($email))) { + $users[] = $this->userResponse($user); + } + } - return $users; - } - ], - 'api' => function () { - return [ - [ - 'pattern' => '/', - 'action' => function () { - $field = $this->field(); + return $users; + } + ], + 'api' => function () { + return [ + [ + 'pattern' => '/', + 'action' => function () { + $field = $this->field(); - return $field->userpicker([ - 'image' => $field->image(), - 'info' => $field->info(), - 'layout' => $field->layout(), - 'limit' => $field->limit(), - 'page' => $this->requestQuery('page'), - 'query' => $field->query(), - 'search' => $this->requestQuery('search'), - 'text' => $field->text() - ]); - } - ] - ]; - }, - 'save' => function ($value = null) { - return A::pluck($value, 'id'); - }, - 'validations' => [ - 'max', - 'min' - ] + return $field->userpicker([ + 'image' => $field->image(), + 'info' => $field->info(), + 'layout' => $field->layout(), + 'limit' => $field->limit(), + 'page' => $this->requestQuery('page'), + 'query' => $field->query(), + 'search' => $this->requestQuery('search'), + 'text' => $field->text() + ]); + } + ] + ]; + }, + 'save' => function ($value = null) { + return A::pluck($value, 'id'); + }, + 'validations' => [ + 'max', + 'min' + ] ]; diff --git a/kirby/config/fields/writer.php b/kirby/config/fields/writer.php index a19e9b0..73c4976 100644 --- a/kirby/config/fields/writer.php +++ b/kirby/config/fields/writer.php @@ -3,34 +3,34 @@ use Kirby\Sane\Sane; return [ - 'props' => [ - /** - * Enables inline mode, which will not wrap new lines in paragraphs and creates hard breaks instead. - * - * @param bool $inline - */ - 'inline' => function (bool $inline = false) { - return $inline; - }, - /** - * Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate them all by passing `true`. Deactivate them all by passing `false` - * @param array|bool $marks - */ - 'marks' => function ($marks = true) { - return $marks; - }, - /** - * Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`. - * @param array|bool|null $nodes - */ - 'nodes' => function ($nodes = null) { - return $nodes; - } - ], - 'computed' => [ - 'value' => function () { - $value = trim($this->value ?? ''); - return Sane::sanitize($value, 'html'); - } - ], + 'props' => [ + /** + * Enables inline mode, which will not wrap new lines in paragraphs and creates hard breaks instead. + * + * @param bool $inline + */ + 'inline' => function (bool $inline = false) { + return $inline; + }, + /** + * Sets the allowed HTML formats. Available formats: `bold`, `italic`, `underline`, `strike`, `code`, `link`, `email`. Activate them all by passing `true`. Deactivate them all by passing `false` + * @param array|bool $marks + */ + 'marks' => function ($marks = true) { + return $marks; + }, + /** + * Sets the allowed nodes. Available nodes: `paragraph`, `heading`, `bulletList`, `orderedList`. Activate/deactivate them all by passing `true`/`false`. Default nodes are `paragraph`, `heading`, `bulletList`, `orderedList`. + * @param array|bool|null $nodes + */ + 'nodes' => function ($nodes = null) { + return $nodes; + } + ], + 'computed' => [ + 'value' => function () { + $value = trim($this->value ?? ''); + return Sane::sanitize($value, 'html'); + } + ], ]; diff --git a/kirby/config/helpers.php b/kirby/config/helpers.php index df2b14c..f8d0476 100644 --- a/kirby/config/helpers.php +++ b/kirby/config/helpers.php @@ -1,955 +1,771 @@ collection($name); +if (Helpers::hasOverride('collection') === false) { // @codeCoverageIgnore + /** + * Returns the result of a collection by name + * + * @param string $name + * @return \Kirby\Cms\Collection|null + */ + function collection(string $name) + { + return App::instance()->collection($name); + } } -/** - * Checks / returns a CSRF token - * - * @param string|null $check Pass a token here to compare it to the one in the session - * @return string|bool Either the token or a boolean check result - */ -function csrf(?string $check = null) -{ - $session = App::instance()->session(); +if (Helpers::hasOverride('csrf') === false) { // @codeCoverageIgnore + /** + * Checks / returns a CSRF token + * + * @param string|null $check Pass a token here to compare it to the one in the session + * @return string|bool Either the token or a boolean check result + */ + function csrf(?string $check = null) + { + // check explicitly if there have been no arguments at all; + // checking for null introduces a security issue because null could come + // from user input or bugs in the calling code! + if (func_num_args() === 0) { + return App::instance()->csrf(); + } - // no arguments, generate/return a token - // (check explicitly if there have been no arguments at all; - // checking for null introduces a security issue because null could come - // from user input or bugs in the calling code!) - if (func_num_args() === 0) { - $token = $session->get('kirby.csrf'); - - if (is_string($token) !== true) { - $token = bin2hex(random_bytes(32)); - $session->set('kirby.csrf', $token); - } - - return $token; - } - - // argument has been passed, check the token - if ( - is_string($check) === true && - is_string($session->get('kirby.csrf')) === true - ) { - return hash_equals($session->get('kirby.csrf'), $check) === true; - } - - return false; + return App::instance()->csrf($check); + } } -/** - * Creates one or multiple CSS link tags - * - * @param string|array $url Relative or absolute URLs, an array of URLs or `@auto` for automatic template css loading - * @param string|array $options Pass an array of attributes for the link tag or a media attribute string - * @return string|null - */ -function css($url, $options = null): ?string -{ - if (is_array($url) === true) { - $links = A::map($url, fn ($url) => css($url, $options)); - return implode(PHP_EOL, $links); - } - - if (is_string($options) === true) { - $options = ['media' => $options]; - } - - $kirby = App::instance(); - - if ($url === '@auto') { - if (!$url = Url::toTemplateAsset('css/templates', 'css')) { - return null; - } - } - - // only valid value for 'rel' is 'alternate stylesheet', if 'title' is given as well - if ( - ($options['rel'] ?? '') !== 'alternate stylesheet' || - ($options['title'] ?? '') === '' - ) { - $options['rel'] = 'stylesheet'; - } - - $url = ($kirby->component('css'))($kirby, $url, $options); - $url = Url::to($url); - $attr = array_merge((array)$options, [ - 'href' => $url - ]); - - return ''; +if (Helpers::hasOverride('css') === false) { // @codeCoverageIgnore + /** + * Creates one or multiple CSS link tags + * + * @param string|array $url Relative or absolute URLs, an array of URLs or `@auto` for automatic template css loading + * @param string|array $options Pass an array of attributes for the link tag or a media attribute string + * @return string|null + */ + function css($url, $options = null): ?string + { + return Html::css($url, $options); + } } -/** - * Triggers a deprecation warning if debug mode is active - * @since 3.3.0 - * - * @param string $message - * @return bool Whether the warning was triggered - */ -function deprecated(string $message): bool -{ - if (App::instance()->option('debug') === true) { - return trigger_error($message, E_USER_DEPRECATED) === true; - } - - return false; +if (Helpers::hasOverride('deprecated') === false) { // @codeCoverageIgnore + /** + * Triggers a deprecation warning if debug mode is active + * @since 3.3.0 + * + * @param string $message + * @return bool Whether the warning was triggered + */ + function deprecated(string $message): bool + { + return Helpers::deprecated($message); + } } -if (function_exists('dump') === false) { - /** - * Simple object and variable dumper - * to help with debugging. - * - * @param mixed $variable - * @param bool $echo - * @return string - */ - function dump($variable, bool $echo = true): string - { - $kirby = App::instance(); - return ($kirby->component('dump'))($kirby, $variable, $echo); - } +if (Helpers::hasOverride('dump') === false) { // @codeCoverageIgnore + /** + * Simple object and variable dumper + * to help with debugging. + * + * @param mixed $variable + * @param bool $echo + * @return string + */ + function dump($variable, bool $echo = true): string + { + return Helpers::dump($variable, $echo); + } } -if (function_exists('e') === false) { - /** - * Smart version of echo with an if condition as first argument - * - * @param mixed $condition - * @param mixed $value The string to be echoed if the condition is true - * @param mixed $alternative An alternative string which should be echoed when the condition is false - */ - function e($condition, $value, $alternative = null) - { - echo r($condition, $value, $alternative); - } +if (Helpers::hasOverride('e') === false) { // @codeCoverageIgnore + /** + * Smart version of echo with an if condition as first argument + * + * @param mixed $condition + * @param mixed $value The string to be echoed if the condition is true + * @param mixed $alternative An alternative string which should be echoed when the condition is false + */ + function e($condition, $value, $alternative = null) + { + echo $condition ? $value : $alternative; + } } -/** - * Escape context specific output - * - * @param string $string Untrusted data - * @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`) - * @return string Escaped data - */ -function esc(string $string, string $context = 'html'): string -{ - if (method_exists('Kirby\Toolkit\Escape', $context) === true) { - return Escape::$context($string); - } - - return $string; +if (Helpers::hasOverride('esc') === false) { // @codeCoverageIgnore + /** + * Escape context specific output + * + * @param string $string Untrusted data + * @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`) + * @return string Escaped data + */ + function esc(string $string, string $context = 'html'): string + { + return Str::esc($string, $context); + } } - -/** - * Shortcut for $kirby->request()->get() - * - * @param mixed $key The key to look for. Pass false or null to return the entire request array. - * @param mixed $default Optional default value, which should be returned if no element has been found - * @return mixed - */ -function get($key = null, $default = null) -{ - return App::instance()->request()->get($key, $default); +if (Helpers::hasOverride('get') === false) { // @codeCoverageIgnore + /** + * Shortcut for $kirby->request()->get() + * + * @param mixed $key The key to look for. Pass false or null to return the entire request array. + * @param mixed $default Optional default value, which should be returned if no element has been found + * @return mixed + */ + function get($key = null, $default = null) + { + return App::instance()->request()->get($key, $default); + } } -/** - * Embeds a Github Gist - * - * @param string $url - * @param string|null $file - * @return string - */ -function gist(string $url, ?string $file = null): string -{ - return kirbytag([ - 'gist' => $url, - 'file' => $file, - ]); +if (Helpers::hasOverride('gist') === false) { // @codeCoverageIgnore + /** + * Embeds a Github Gist + * + * @param string $url + * @param string|null $file + * @return string + */ + function gist(string $url, ?string $file = null): string + { + return App::instance()->kirbytag([ + 'gist' => $url, + 'file' => $file, + ]); + } } -/** - * Redirects to the given Urls - * Urls can be relative or absolute. - * - * @param string $url - * @param int $code - * @return void - */ -function go(string $url = '/', int $code = 302) -{ - die(Response::redirect($url, $code)); +if (Helpers::hasOverride('go') === false) { // @codeCoverageIgnore + /** + * Redirects to the given Urls + * Urls can be relative or absolute. + * + * @param string $url + * @param int $code + * @return void + */ + function go(string $url = '/', int $code = 302) + { + Response::go($url, $code); + } } -/** - * Shortcut for html() - * - * @param string|null $string unencoded text - * @param bool $keepTags - * @return string - */ -function h(?string $string, bool $keepTags = false) -{ - return Html::encode($string, $keepTags); +if (Helpers::hasOverride('h') === false) { // @codeCoverageIgnore + /** + * Shortcut for html() + * + * @param string|null $string unencoded text + * @param bool $keepTags + * @return string + */ + function h(?string $string, bool $keepTags = false): string + { + return Html::encode($string, $keepTags); + } } -/** - * Creates safe html by encoding special characters - * - * @param string|null $string unencoded text - * @param bool $keepTags - * @return string - */ -function html(?string $string, bool $keepTags = false) -{ - return Html::encode($string, $keepTags); +if (Helpers::hasOverride('html') === false) { // @codeCoverageIgnore + /** + * Creates safe html by encoding special characters + * + * @param string|null $string unencoded text + * @param bool $keepTags + * @return string + */ + function html(?string $string, bool $keepTags = false): string + { + return Html::encode($string, $keepTags); + } } -/** - * Return an image from any page - * specified by the path - * - * Example: - * - * - * @param string|null $path - * @return \Kirby\Cms\File|null - */ -function image(?string $path = null) -{ - if ($path === null) { - return page()->image(); - } - - $uri = dirname($path); - $filename = basename($path); - - if ($uri === '.') { - $uri = null; - } - - switch ($uri) { - case '/': - $parent = site(); - break; - case null: - $parent = page(); - break; - default: - $parent = page($uri); - break; - } - - if ($parent) { - return $parent->image($filename); - } else { - return null; - } +if (Helpers::hasOverride('image') === false) { // @codeCoverageIgnore + /** + * Return an image from any page + * specified by the path + * + * Example: + * + * + * @param string|null $path + * @return \Kirby\Cms\File|null + */ + function image(?string $path = null) + { + return App::instance()->image($path); + } } -/** - * Runs a number of validators on a set of data and checks if the data is invalid - * - * @param array $data - * @param array $rules - * @param array $messages - * @return array - */ -function invalid(array $data = [], array $rules = [], array $messages = []): array -{ - $errors = []; - - foreach ($rules as $field => $validations) { - $validationIndex = -1; - - // See: http://php.net/manual/en/types.comparisons.php - // only false for: null, undefined variable, '', [] - $value = $data[$field] ?? null; - $filled = $value !== null && $value !== '' && $value !== []; - $message = $messages[$field] ?? $field; - - // True if there is an error message for each validation method. - $messageArray = is_array($message); - - foreach ($validations as $method => $options) { - // If the index is numeric, there is no option - // and `$value` is sent directly as a `$options` parameter - if (is_numeric($method) === true) { - $method = $options; - $options = [$value]; - } else { - if (is_array($options) === false) { - $options = [$options]; - } - - array_unshift($options, $value); - } - - $validationIndex++; - - if ($method === 'required') { - if ($filled) { - // Field is required and filled. - continue; - } - } elseif ($filled) { - if (V::$method(...$options) === true) { - // Field is filled and passes validation method. - continue; - } - } else { - // If a field is not required and not filled, no validation should be done. - continue; - } - - // If no continue was called we have a failed validation. - if ($messageArray) { - $errors[$field][] = $message[$validationIndex] ?? $field; - } else { - $errors[$field] = $message; - } - } - } - - return $errors; +if (Helpers::hasOverride('invalid') === false) { // @codeCoverageIgnore + /** + * Runs a number of validators on a set of data and checks if the data is invalid + * + * @param array $data + * @param array $rules + * @param array $messages + * @return array + */ + function invalid(array $data = [], array $rules = [], array $messages = []): array + { + return V::invalid($data, $rules, $messages); + } } -/** - * Creates a script tag to load a javascript file - * - * @param string|array $url - * @param string|array $options - * @return string|null - */ -function js($url, $options = null): ?string -{ - if (is_array($url) === true) { - $scripts = A::map($url, fn ($url) => js($url, $options)); - return implode(PHP_EOL, $scripts); - } - - if (is_bool($options) === true) { - $options = ['async' => $options]; - } - - $kirby = App::instance(); - - if ($url === '@auto') { - if (!$url = Url::toTemplateAsset('js/templates', 'js')) { - return null; - } - } - - $url = ($kirby->component('js'))($kirby, $url, $options); - $url = Url::to($url); - $attr = array_merge((array)$options, ['src' => $url]); - - return ''; +if (Helpers::hasOverride('js') === false) { // @codeCoverageIgnore + /** + * Creates a script tag to load a javascript file + * + * @param string|array $url + * @param string|array $options + * @return string|null + */ + function js($url, $options = null): ?string + { + return Html::js($url, $options); + } } -/** - * Returns the Kirby object in any situation - * - * @return \Kirby\Cms\App - */ -function kirby() -{ - return App::instance(); +if (Helpers::hasOverride('kirby') === false) { // @codeCoverageIgnore + /** + * Returns the Kirby object in any situation + * + * @return \Kirby\Cms\App + */ + function kirby() + { + return App::instance(); + } } -/** - * Makes it possible to use any defined Kirbytag as standalone function - * - * @param string|array $type - * @param string|null $value - * @param array $attr - * @param array $data - * @return string - */ -function kirbytag($type, ?string $value = null, array $attr = [], array $data = []): string -{ - if (is_array($type) === true) { - $kirbytag = $type; - $type = key($kirbytag); - $value = current($kirbytag); - $attr = $kirbytag; - - // check data attribute and separate from attr data if exists - if (isset($attr['data']) === true) { - $data = $attr['data']; - unset($attr['data']); - } - } - - return App::instance()->kirbytag($type, $value, $attr, $data); +if (Helpers::hasOverride('kirbytag') === false) { // @codeCoverageIgnore + /** + * Makes it possible to use any defined Kirbytag as standalone function + * + * @param string|array $type + * @param string|null $value + * @param array $attr + * @param array $data + * @return string + */ + function kirbytag($type, ?string $value = null, array $attr = [], array $data = []): string + { + return App::instance()->kirbytag($type, $value, $attr, $data); + } } -/** - * Parses KirbyTags in the given string. Shortcut - * for `$kirby->kirbytags($text, $data)` - * - * @param string|null $text - * @param array $data - * @return string - */ -function kirbytags(?string $text = null, array $data = []): string -{ - return App::instance()->kirbytags($text, $data); +if (Helpers::hasOverride('kirbytags') === false) { // @codeCoverageIgnore + /** + * Parses KirbyTags in the given string. Shortcut + * for `$kirby->kirbytags($text, $data)` + * + * @param string|null $text + * @param array $data + * @return string + */ + function kirbytags(?string $text = null, array $data = []): string + { + return App::instance()->kirbytags($text, $data); + } } -/** - * Parses KirbyTags and Markdown in the - * given string. Shortcut for `$kirby->kirbytext()` - * - * @param string|null $text - * @param array $data - * @return string - */ -function kirbytext(?string $text = null, array $data = []): string -{ - return App::instance()->kirbytext($text, $data); +if (Helpers::hasOverride('kirbytext') === false) { // @codeCoverageIgnore + /** + * Parses KirbyTags and Markdown in the + * given string. Shortcut for `$kirby->kirbytext()` + * + * @param string|null $text + * @param array $data + * @return string + */ + function kirbytext(?string $text = null, array $data = []): string + { + return App::instance()->kirbytext($text, $data); + } } -/** - * Parses KirbyTags and inline Markdown in the - * given string. - * @since 3.1.0 - * - * @param string|null $text - * @param array $data - * @return string - */ -function kirbytextinline(?string $text = null, array $data = []): string -{ - return App::instance()->kirbytext($text, $data, true); +if (Helpers::hasOverride('kirbytextinline') === false) { // @codeCoverageIgnore + /** + * Parses KirbyTags and inline Markdown in the + * given string. + * @since 3.1.0 + * + * @param string|null $text + * @param array $options + * @return string + */ + function kirbytextinline(?string $text = null, array $options = []): string + { + $options['markdown']['inline'] = true; + return App::instance()->kirbytext($text, $options); + } } -/** - * Shortcut for `kirbytext()` helper - * - * @param string|null $text - * @param array $data - * @return string - */ -function kt(?string $text = null, array $data = []): string -{ - return kirbytext($text, $data); +if (Helpers::hasOverride('kt') === false) { // @codeCoverageIgnore + /** + * Shortcut for `kirbytext()` helper + * + * @param string|null $text + * @param array $data + * @return string + */ + function kt(?string $text = null, array $data = []): string + { + return App::instance()->kirbytext($text, $data); + } } -/** - * Shortcut for `kirbytextinline()` helper - * @since 3.1.0 - * - * @param string|null $text - * @param array $data - * @return string - */ -function kti(?string $text = null, array $data = []): string -{ - return kirbytextinline($text, $data); +if (Helpers::hasOverride('kti') === false) { // @codeCoverageIgnore + /** + * Shortcut for `kirbytextinline()` helper + * @since 3.1.0 + * + * @param string|null $text + * @param array $options + * @return string + */ + function kti(?string $text = null, array $options = []): string + { + $options['markdown']['inline'] = true; + return App::instance()->kirbytext($text, $options); + } } -/** - * A super simple class autoloader - * - * @param array $classmap - * @param string|null $base - * @return void - */ -function load(array $classmap, ?string $base = null) -{ - // convert all classnames to lowercase - $classmap = array_change_key_case($classmap); - - spl_autoload_register(function ($class) use ($classmap, $base) { - $class = strtolower($class); - - if (!isset($classmap[$class])) { - return false; - } - - if ($base) { - include $base . '/' . $classmap[$class]; - } else { - include $classmap[$class]; - } - }); +if (Helpers::hasOverride('load') === false) { // @codeCoverageIgnore + /** + * A super simple class autoloader + * + * @param array $classmap + * @param string|null $base + * @return void + */ + function load(array $classmap, ?string $base = null): void + { + F::loadClasses($classmap, $base); + } } -/** - * Parses markdown in the given string. Shortcut for - * `$kirby->markdown($text)` - * - * @param string|null $text - * @param array $options - * @return string - */ -function markdown(?string $text = null, array $options = []): string -{ - return App::instance()->markdown($text, $options); +if (Helpers::hasOverride('markdown') === false) { // @codeCoverageIgnore + /** + * Parses markdown in the given string. Shortcut for + * `$kirby->markdown($text)` + * + * @param string|null $text + * @param array $options + * @return string + */ + function markdown(?string $text = null, array $options = []): string + { + return App::instance()->markdown($text, $options); + } } -/** - * Shortcut for `$kirby->option($key, $default)` - * - * @param string $key - * @param mixed $default - * @return mixed - */ -function option(string $key, $default = null) -{ - return App::instance()->option($key, $default); +if (Helpers::hasOverride('option') === false) { // @codeCoverageIgnore + /** + * Shortcut for `$kirby->option($key, $default)` + * + * @param string $key + * @param mixed $default + * @return mixed + */ + function option(string $key, $default = null) + { + return App::instance()->option($key, $default); + } } -/** - * Fetches a single page or multiple pages by - * id or the current page when no id is specified - * - * @param string|array ...$id - * @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null - * @todo reduce to one parameter in 3.7.0 (also change return and return type) - */ -function page(...$id) -{ - if (empty($id) === true) { - return App::instance()->site()->page(); - } +if (Helpers::hasOverride('page') === false) { // @codeCoverageIgnore + /** + * Fetches a single page by id or + * the current page when no id is specified + * + * @param string|null $id + * @return \Kirby\Cms\Page|null + */ + function page(?string $id = null) + { + if (empty($id) === true) { + return App::instance()->site()->page(); + } - if (count($id) > 1) { - // @codeCoverageIgnoreStart - deprecated('Passing multiple parameters to the `page()` helper has been deprecated. Please use the `pages()` helper instead.'); - // @codeCoverageIgnoreEnd - } - - return App::instance()->site()->find(...$id); + return App::instance()->site()->find($id); + } } -/** - * Helper to build page collections - * - * @param string|array ...$id - * @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null - * @todo return only Pages|null in 3.7.0, wrap in Pages for single passed id - */ -function pages(...$id) -{ - if (count($id) === 1 && is_array($id[0]) === false) { - // @codeCoverageIgnoreStart - deprecated('Passing a single id to the `pages()` helper will return a Kirby\Cms\Pages collection with a single element instead of the single Kirby\Cms\Page object itself - starting in 3.7.0.'); - // @codeCoverageIgnoreEnd - } +if (Helpers::hasOverride('pages') === false) { // @codeCoverageIgnore + /** + * Helper to build pages collection + * + * @param string|array ...$id + * @return \Kirby\Cms\Pages|null + */ + function pages(...$id) + { + // ensure that a list of string arguments and an array + // as the first argument are treated the same + if (count($id) === 1 && is_array($id[0]) === true) { + $id = $id[0]; + } - return App::instance()->site()->find(...$id); + // always passes $id an array; ensures we get a + // collection even if only one ID is passed + return App::instance()->site()->find($id); + } } -/** - * Returns a single param from the URL - * - * @param string $key - * @param string|null $fallback - * @return string|null - */ -function param(string $key, ?string $fallback = null): ?string -{ - return App::instance()->request()->url()->params()->$key ?? $fallback; +if (Helpers::hasOverride('param') === false) { // @codeCoverageIgnore + /** + * Returns a single param from the URL + * + * @param string $key + * @param string|null $fallback + * @return string|null + */ + function param(string $key, ?string $fallback = null): ?string + { + return App::instance()->request()->url()->params()->$key ?? $fallback; + } } -/** - * Returns all params from the current Url - * - * @return array - */ -function params(): array -{ - return App::instance()->request()->url()->params()->toArray(); +if (Helpers::hasOverride('params') === false) { // @codeCoverageIgnore + /** + * Returns all params from the current Url + * + * @return array + */ + function params(): array + { + return App::instance()->request()->url()->params()->toArray(); + } } -/** - * Smart version of return with an if condition as first argument - * - * @param mixed $condition - * @param mixed $value The string to be returned if the condition is true - * @param mixed $alternative An alternative string which should be returned when the condition is false - * @return mixed - */ -function r($condition, $value, $alternative = null) -{ - return $condition ? $value : $alternative; +if (Helpers::hasOverride('r') === false) { // @codeCoverageIgnore + /** + * Smart version of return with an if condition as first argument + * + * @param mixed $condition + * @param mixed $value The string to be returned if the condition is true + * @param mixed $alternative An alternative string which should be returned when the condition is false + * @return mixed + */ + function r($condition, $value, $alternative = null) + { + return $condition ? $value : $alternative; + } } -/** - * Creates a micro-router and executes - * the routing action immediately - * @since 3.6.0 - * - * @param string|null $path - * @param string $method - * @param array $routes - * @param \Closure|null $callback - * @return mixed - */ -function router(?string $path = null, string $method = 'GET', array $routes = [], ?Closure $callback = null) -{ - return (new Router($routes))->call($path, $method, $callback); +if (Helpers::hasOverride('router') === false) { // @codeCoverageIgnore + /** + * Creates a micro-router and executes + * the routing action immediately + * @since 3.6.0 + * + * @param string|null $path + * @param string $method + * @param array $routes + * @param \Closure|null $callback + * @return mixed + */ + function router(?string $path = null, string $method = 'GET', array $routes = [], ?Closure $callback = null) + { + return Router::execute($path, $method, $routes, $callback); + } } -/** - * Returns the current site object - * - * @return \Kirby\Cms\Site - */ -function site() -{ - return App::instance()->site(); +if (Helpers::hasOverride('site') === false) { // @codeCoverageIgnore + /** + * Returns the current site object + * + * @return \Kirby\Cms\Site + */ + function site() + { + return App::instance()->site(); + } } -/** - * Determines the size/length of numbers, strings, arrays and countable objects - * - * @param mixed $value - * @return int - * @throws \Kirby\Exception\InvalidArgumentException - */ -function size($value): int -{ - if (is_numeric($value)) { - return (int)$value; - } - - if (is_string($value)) { - return Str::length(trim($value)); - } - - if (is_array($value)) { - return count($value); - } - - if (is_object($value)) { - if (is_a($value, 'Countable') === true) { - return count($value); - } - - if (is_a($value, 'Kirby\Toolkit\Collection') === true) { - return $value->count(); - } - } - - throw new InvalidArgumentException('Could not determine the size of the given value'); +if (Helpers::hasOverride('size') === false) { // @codeCoverageIgnore + /** + * Determines the size/length of numbers, strings, arrays and countable objects + * + * @param mixed $value + * @return int + * @throws \Kirby\Exception\InvalidArgumentException + */ + function size($value): int + { + return Helpers::size($value); + } } -/** - * Enhances the given string with - * smartypants. Shortcut for `$kirby->smartypants($text)` - * - * @param string|null $text - * @return string - */ -function smartypants(?string $text = null): string -{ - return App::instance()->smartypants($text); +if (Helpers::hasOverride('smartypants') === false) { // @codeCoverageIgnore + /** + * Enhances the given string with + * smartypants. Shortcut for `$kirby->smartypants($text)` + * + * @param string|null $text + * @return string + */ + function smartypants(?string $text = null): string + { + return App::instance()->smartypants($text); + } } -/** - * Embeds a snippet from the snippet folder - * - * @param string|array $name - * @param array|object $data - * @param bool $return - * @return string - */ -function snippet($name, $data = [], bool $return = false) -{ - if (is_object($data) === true) { - $data = ['item' => $data]; - } - - $snippet = App::instance()->snippet($name, $data); - - if ($return === true) { - return $snippet; - } - - echo $snippet; +if (Helpers::hasOverride('snippet') === false) { // @codeCoverageIgnore + /** + * Embeds a snippet from the snippet folder + * + * @param string|array $name + * @param array|object $data + * @param bool $return + * @return string|null + */ + function snippet($name, $data = [], bool $return = false): ?string + { + return App::instance()->snippet($name, $data, $return); + } } -/** - * Includes an SVG file by absolute or - * relative file path. - * - * @param string|\Kirby\Cms\File $file - * @return string|false - */ -function svg($file) -{ - // support for Kirby's file objects - if (is_a($file, 'Kirby\Cms\File') === true && $file->extension() === 'svg') { - return $file->read(); - } - - if (is_string($file) === false) { - return false; - } - - $extension = F::extension($file); - - // check for valid svg files - if ($extension !== 'svg') { - return false; - } - - // try to convert relative paths to absolute - if (file_exists($file) === false) { - $root = App::instance()->root(); - $file = realpath($root . '/' . $file); - } - - return F::read($file); +if (Helpers::hasOverride('svg') === false) { // @codeCoverageIgnore + /** + * Includes an SVG file by absolute or + * relative file path. + * + * @param string|\Kirby\Cms\File $file + * @return string|false + */ + function svg($file) + { + return Html::svg($file); + } } -/** - * Returns translate string for key from translation file - * - * @param string|array $key - * @param string|null $fallback - * @param string|null $locale - * @return array|string|null - */ -function t($key, string $fallback = null, string $locale = null) -{ - return I18n::translate($key, $fallback, $locale); +if (Helpers::hasOverride('t') === false) { // @codeCoverageIgnore + /** + * Returns translate string for key from translation file + * + * @param string|array $key + * @param string|null $fallback + * @param string|null $locale + * @return array|string|null + */ + function t($key, string $fallback = null, string $locale = null) + { + return I18n::translate($key, $fallback, $locale); + } } -/** - * Translates a count - * - * @param string $key - * @param int $count - * @param string|null $locale - * @param bool $formatNumber If set to `false`, the count is not formatted - * @return mixed - */ -function tc(string $key, int $count, string $locale = null, bool $formatNumber = true) -{ - return I18n::translateCount($key, $count, $locale, $formatNumber); +if (Helpers::hasOverride('tc') === false) { // @codeCoverageIgnore + /** + * Translates a count + * + * @param string $key + * @param int $count + * @param string|null $locale + * @param bool $formatNumber If set to `false`, the count is not formatted + * @return mixed + */ + function tc( + string $key, + int $count, + string $locale = null, + bool $formatNumber = true + ) { + return I18n::translateCount($key, $count, $locale, $formatNumber); + } } -/** - * Rounds the minutes of the given date - * by the defined step - * - * @param string|null $date - * @param int|array|null $step array of `unit` and `size` to round to nearest - * @return int|null - */ -function timestamp(?string $date = null, $step = null): ?int -{ - if ($date = Date::optional($date)) { - if ($step !== null) { - $step = Date::stepConfig($step, [ - 'unit' => 'minute', - 'size' => 1 - ]); - $date->round($step['unit'], $step['size']); - } - - return $date->timestamp(); - } - - return null; +if (Helpers::hasOverride('timestamp') === false) { // @codeCoverageIgnore + /** + * Rounds the minutes of the given date + * by the defined step + * + * @param string|null $date + * @param int|array|null $step array of `unit` and `size` to round to nearest + * @return int|null + */ + function timestamp(?string $date = null, $step = null): ?int + { + return Date::roundedTimestamp($date, $step); + } } -/** - * Translate by key and then replace - * placeholders in the text - * - * @param string $key - * @param string|array|null $fallback - * @param array|null $replace - * @param string|null $locale - * @return string - */ -function tt(string $key, $fallback = null, ?array $replace = null, ?string $locale = null) -{ - return I18n::template($key, $fallback, $replace, $locale); +if (Helpers::hasOverride('tt') === false) { // @codeCoverageIgnore + /** + * Translate by key and then replace + * placeholders in the text + * + * @param string $key + * @param string|array|null $fallback + * @param array|null $replace + * @param string|null $locale + * @return string + */ + function tt(string $key, $fallback = null, ?array $replace = null, ?string $locale = null): string + { + return I18n::template($key, $fallback, $replace, $locale); + } } -/** - * Builds a Twitter link - * - * @param string $username - * @param string|null $text - * @param string|null $title - * @param string|null $class - * @return string - */ -function twitter(string $username, ?string $text = null, ?string $title = null, ?string $class = null): string -{ - return kirbytag([ - 'twitter' => $username, - 'text' => $text, - 'title' => $title, - 'class' => $class - ]); +if (Helpers::hasOverride('twitter') === false) { // @codeCoverageIgnore + /** + * Builds a Twitter link + * + * @param string $username + * @param string|null $text + * @param string|null $title + * @param string|null $class + * @return string + */ + function twitter(string $username, ?string $text = null, ?string $title = null, ?string $class = null): string + { + return App::instance()->kirbytag([ + 'twitter' => $username, + 'text' => $text, + 'title' => $title, + 'class' => $class + ]); + } } -/** - * Shortcut for url() - * - * @param string|null $path - * @param array|string|null $options - * @return string - */ -function u(?string $path = null, $options = null): string -{ - return Url::to($path, $options); +if (Helpers::hasOverride('u') === false) { // @codeCoverageIgnore + /** + * Shortcut for url() + * + * @param string|null $path + * @param array|string|null $options + * @return string + */ + function u(?string $path = null, $options = null): string + { + return Url::to($path, $options); + } } -/** - * Builds an absolute URL for a given path - * - * @param string|null $path - * @param array|string|null $options - * @return string - */ -function url(?string $path = null, $options = null): string -{ - return Url::to($path, $options); +if (Helpers::hasOverride('url') === false) { // @codeCoverageIgnore + /** + * Builds an absolute URL for a given path + * + * @param string|null $path + * @param array|string|null $options + * @return string + */ + function url(?string $path = null, $options = null): string + { + return Url::to($path, $options); + } } -/** - * Creates a compliant v4 UUID - * Taken from: https://github.com/symfony/polyfill - * - * @return string - */ -function uuid(): string -{ - $uuid = bin2hex(random_bytes(16)); - - return sprintf( - '%08s-%04s-4%03s-%04x-%012s', - // 32 bits for "time_low" - substr($uuid, 0, 8), - // 16 bits for "time_mid" - substr($uuid, 8, 4), - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - substr($uuid, 13, 3), - // 16 bits: - // * 8 bits for "clk_seq_hi_res", - // * 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - hexdec(substr($uuid, 16, 4)) & 0x3fff | 0x8000, - // 48 bits for "node" - substr($uuid, 20, 12) - ); +if (Helpers::hasOverride('uuid') === false) { // @codeCoverageIgnore + /** + * Creates a compliant v4 UUID + * + * @return string + */ + function uuid(): string + { + return Str::uuid(); + } } - -/** - * Creates a video embed via iframe for Youtube or Vimeo - * videos. The embed Urls are automatically detected from - * the given Url. - * - * @param string $url - * @param array $options - * @param array $attr - * @return string|null - */ -function video(string $url, array $options = [], array $attr = []): ?string -{ - return Html::video($url, $options, $attr); +if (Helpers::hasOverride('video') === false) { // @codeCoverageIgnore + /** + * Creates a video embed via iframe for Youtube or Vimeo + * videos. The embed Urls are automatically detected from + * the given Url. + * + * @param string $url + * @param array $options + * @param array $attr + * @return string|null + */ + function video(string $url, array $options = [], array $attr = []): ?string + { + return Html::video($url, $options, $attr); + } } -/** - * Embeds a Vimeo video by URL in an iframe - * - * @param string $url - * @param array $options - * @param array $attr - * @return string|null - */ -function vimeo(string $url, array $options = [], array $attr = []): ?string -{ - return Html::vimeo($url, $options, $attr); +if (Helpers::hasOverride('vimeo') === false) { // @codeCoverageIgnore + /** + * Embeds a Vimeo video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string|null + */ + function vimeo(string $url, array $options = [], array $attr = []): ?string + { + return Html::vimeo($url, $options, $attr); + } } -/** - * The widont function makes sure that there are no - * typographical widows at the end of a paragraph – - * that's a single word in the last line - * - * @param string|null $string - * @return string - */ -function widont(string $string = null): string -{ - return Str::widont($string); +if (Helpers::hasOverride('widont') === false) { // @codeCoverageIgnore + /** + * The widont function makes sure that there are no + * typographical widows at the end of a paragraph – + * that's a single word in the last line + * + * @param string|null $string + * @return string + */ + function widont(string $string = null): string + { + return Str::widont($string); + } } -/** - * Embeds a Youtube video by URL in an iframe - * - * @param string $url - * @param array $options - * @param array $attr - * @return string|null - */ -function youtube(string $url, array $options = [], array $attr = []): ?string -{ - return Html::youtube($url, $options, $attr); +if (Helpers::hasOverride('youtube') === false) { // @codeCoverageIgnore + /** + * Embeds a Youtube video by URL in an iframe + * + * @param string $url + * @param array $options + * @param array $attr + * @return string|null + */ + function youtube(string $url, array $options = [], array $attr = []): ?string + { + return Html::youtube($url, $options, $attr); + } } diff --git a/kirby/config/methods.php b/kirby/config/methods.php index 94fef2f..00209c0 100644 --- a/kirby/config/methods.php +++ b/kirby/config/methods.php @@ -19,596 +19,596 @@ use Kirby\Toolkit\Xml; * Field method setup */ return function (App $app) { - return [ + return [ - // states + // states - /** - * Converts the field value into a proper boolean and inverts it - * - * @param \Kirby\Cms\Field $field - * @return bool - */ - 'isFalse' => function (Field $field): bool { - return $field->toBool() === false; - }, + /** + * Converts the field value into a proper boolean and inverts it + * + * @param \Kirby\Cms\Field $field + * @return bool + */ + 'isFalse' => function (Field $field): bool { + return $field->toBool() === false; + }, - /** - * Converts the field value into a proper boolean - * - * @param \Kirby\Cms\Field $field - * @return bool - */ - 'isTrue' => function (Field $field): bool { - return $field->toBool() === true; - }, + /** + * Converts the field value into a proper boolean + * + * @param \Kirby\Cms\Field $field + * @return bool + */ + 'isTrue' => function (Field $field): bool { + return $field->toBool() === true; + }, - /** - * Validates the field content with the given validator and parameters - * - * @param string $validator - * @param mixed ...$arguments A list of optional validator arguments - * @return bool - */ - 'isValid' => function (Field $field, string $validator, ...$arguments): bool { - return V::$validator($field->value, ...$arguments); - }, + /** + * Validates the field content with the given validator and parameters + * + * @param string $validator + * @param mixed ...$arguments A list of optional validator arguments + * @return bool + */ + 'isValid' => function (Field $field, string $validator, ...$arguments): bool { + return V::$validator($field->value, ...$arguments); + }, - // converters - /** - * Converts a yaml or json field to a Blocks object - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Blocks - */ - 'toBlocks' => function (Field $field) { - try { - $blocks = Blocks::factory(Blocks::parse($field->value()), [ - 'parent' => $field->parent(), - ]); + // converters + /** + * Converts a yaml or json field to a Blocks object + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Blocks + */ + 'toBlocks' => function (Field $field) { + try { + $blocks = Blocks::factory(Blocks::parse($field->value()), [ + 'parent' => $field->parent(), + ]); - return $blocks->filter('isHidden', false); - } catch (Throwable $e) { - if ($field->parent() === null) { - $message = 'Invalid blocks data for "' . $field->key() . '" field'; - } else { - $message = 'Invalid blocks data for "' . $field->key() . '" field on parent "' . $field->parent()->title() . '"'; - } + return $blocks->filter('isHidden', false); + } catch (Throwable $e) { + if ($field->parent() === null) { + $message = 'Invalid blocks data for "' . $field->key() . '" field'; + } else { + $message = 'Invalid blocks data for "' . $field->key() . '" field on parent "' . $field->parent()->title() . '"'; + } - throw new InvalidArgumentException($message); - } - }, + throw new InvalidArgumentException($message); + } + }, - /** - * Converts the field value into a proper boolean - * - * @param \Kirby\Cms\Field $field - * @param bool $default Default value if the field is empty - * @return bool - */ - 'toBool' => function (Field $field, $default = false): bool { - $value = $field->isEmpty() ? $default : $field->value; - return filter_var($value, FILTER_VALIDATE_BOOLEAN); - }, + /** + * Converts the field value into a proper boolean + * + * @param \Kirby\Cms\Field $field + * @param bool $default Default value if the field is empty + * @return bool + */ + 'toBool' => function (Field $field, $default = false): bool { + $value = $field->isEmpty() ? $default : $field->value; + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + }, - /** - * Parses the field value with the given method - * - * @param \Kirby\Cms\Field $field - * @param string $method [',', 'yaml', 'json'] - * @return array - */ - 'toData' => function (Field $field, string $method = ',') { - switch ($method) { - case 'yaml': - case 'json': - return Data::decode($field->value, $method); - default: - return $field->split($method); - } - }, + /** + * Parses the field value with the given method + * + * @param \Kirby\Cms\Field $field + * @param string $method [',', 'yaml', 'json'] + * @return array + */ + 'toData' => function (Field $field, string $method = ',') { + switch ($method) { + case 'yaml': + case 'json': + return Data::decode($field->value, $method); + default: + return $field->split($method); + } + }, - /** - * Converts the field value to a timestamp or a formatted date - * - * @param \Kirby\Cms\Field $field - * @param string|null $format PHP date formatting string - * @param string|null $fallback Fallback string for `strtotime` (since 3.2) - * @return string|int - */ - 'toDate' => function (Field $field, string $format = null, string $fallback = null) use ($app) { - if (empty($field->value) === true && $fallback === null) { - return null; - } + /** + * Converts the field value to a timestamp or a formatted date + * + * @param \Kirby\Cms\Field $field + * @param string|\IntlDateFormatter|null $format PHP date formatting string + * @param string|null $fallback Fallback string for `strtotime` (since 3.2) + * @return string|int + */ + 'toDate' => function (Field $field, $format = null, string $fallback = null) use ($app) { + if (empty($field->value) === true && $fallback === null) { + return null; + } - if (empty($field->value) === false) { - $time = $field->toTimestamp(); - } else { - $time = strtotime($fallback); - } + if (empty($field->value) === false) { + $time = $field->toTimestamp(); + } else { + $time = strtotime($fallback); + } - $handler = $app->option('date.handler', 'date'); - return Str::date($time, $format, $handler); - }, + $handler = $app->option('date.handler', 'date'); + return Str::date($time, $format, $handler); + }, - /** - * Returns a file object from a filename in the field - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\File|null - */ - 'toFile' => function (Field $field) { - return $field->toFiles()->first(); - }, + /** + * Returns a file object from a filename in the field + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\File|null + */ + 'toFile' => function (Field $field) { + return $field->toFiles()->first(); + }, - /** - * Returns a file collection from a yaml list of filenames in the field - * - * @param \Kirby\Cms\Field $field - * @param string $separator - * @return \Kirby\Cms\Files - */ - 'toFiles' => function (Field $field, string $separator = 'yaml') { - $parent = $field->parent(); - $files = new Files([]); + /** + * Returns a file collection from a yaml list of filenames in the field + * + * @param \Kirby\Cms\Field $field + * @param string $separator + * @return \Kirby\Cms\Files + */ + 'toFiles' => function (Field $field, string $separator = 'yaml') { + $parent = $field->parent(); + $files = new Files([]); - foreach ($field->toData($separator) as $id) { - if ($file = $parent->kirby()->file($id, $parent)) { - $files->add($file); - } - } + foreach ($field->toData($separator) as $id) { + if ($file = $parent->kirby()->file($id, $parent)) { + $files->add($file); + } + } - return $files; - }, + return $files; + }, - /** - * Converts the field value into a proper float - * - * @param \Kirby\Cms\Field $field - * @param float $default Default value if the field is empty - * @return float - */ - 'toFloat' => function (Field $field, float $default = 0) { - $value = $field->isEmpty() ? $default : $field->value; - return (float)$value; - }, + /** + * Converts the field value into a proper float + * + * @param \Kirby\Cms\Field $field + * @param float $default Default value if the field is empty + * @return float + */ + 'toFloat' => function (Field $field, float $default = 0) { + $value = $field->isEmpty() ? $default : $field->value; + return (float)$value; + }, - /** - * Converts the field value into a proper integer - * - * @param \Kirby\Cms\Field $field - * @param int $default Default value if the field is empty - * @return int - */ - 'toInt' => function (Field $field, int $default = 0) { - $value = $field->isEmpty() ? $default : $field->value; - return (int)$value; - }, + /** + * Converts the field value into a proper integer + * + * @param \Kirby\Cms\Field $field + * @param int $default Default value if the field is empty + * @return int + */ + 'toInt' => function (Field $field, int $default = 0) { + $value = $field->isEmpty() ? $default : $field->value; + return (int)$value; + }, - /** - * Parse layouts and turn them into - * Layout objects - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Layouts - */ - 'toLayouts' => function (Field $field) { - return Layouts::factory(Layouts::parse($field->value()), [ - 'parent' => $field->parent() - ]); - }, + /** + * Parse layouts and turn them into + * Layout objects + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Layouts + */ + 'toLayouts' => function (Field $field) { + return Layouts::factory(Layouts::parse($field->value()), [ + 'parent' => $field->parent() + ]); + }, - /** - * Wraps a link tag around the field value. The field value is used as the link text - * - * @param \Kirby\Cms\Field $field - * @param mixed $attr1 Can be an optional Url. If no Url is set, the Url of the Page, File or Site will be used. Can also be an array of link attributes - * @param mixed $attr2 If `$attr1` is used to set the Url, you can use `$attr2` to pass an array of additional attributes. - * @return string - */ - 'toLink' => function (Field $field, $attr1 = null, $attr2 = null) { - if (is_string($attr1) === true) { - $href = $attr1; - $attr = $attr2; - } else { - $href = $field->parent()->url(); - $attr = $attr1; - } + /** + * Wraps a link tag around the field value. The field value is used as the link text + * + * @param \Kirby\Cms\Field $field + * @param mixed $attr1 Can be an optional Url. If no Url is set, the Url of the Page, File or Site will be used. Can also be an array of link attributes + * @param mixed $attr2 If `$attr1` is used to set the Url, you can use `$attr2` to pass an array of additional attributes. + * @return string + */ + 'toLink' => function (Field $field, $attr1 = null, $attr2 = null) { + if (is_string($attr1) === true) { + $href = $attr1; + $attr = $attr2; + } else { + $href = $field->parent()->url(); + $attr = $attr1; + } - if ($field->parent()->isActive()) { - $attr['aria-current'] = 'page'; - } + if ($field->parent()->isActive()) { + $attr['aria-current'] = 'page'; + } - return Html::a($href, $field->value, $attr ?? []); - }, + return Html::a($href, $field->value, $attr ?? []); + }, - /** - * Returns a page object from a page id in the field - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Page|null - */ - 'toPage' => function (Field $field) { - return $field->toPages()->first(); - }, + /** + * Returns a page object from a page id in the field + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Page|null + */ + 'toPage' => function (Field $field) { + return $field->toPages()->first(); + }, - /** - * Returns a pages collection from a yaml list of page ids in the field - * - * @param \Kirby\Cms\Field $field - * @param string $separator Can be any other separator to split the field value by - * @return \Kirby\Cms\Pages - */ - 'toPages' => function (Field $field, string $separator = 'yaml') use ($app) { - return $app->site()->find(false, false, ...$field->toData($separator)); - }, + /** + * Returns a pages collection from a yaml list of page ids in the field + * + * @param \Kirby\Cms\Field $field + * @param string $separator Can be any other separator to split the field value by + * @return \Kirby\Cms\Pages + */ + 'toPages' => function (Field $field, string $separator = 'yaml') use ($app) { + return $app->site()->find(false, false, ...$field->toData($separator)); + }, - /** - * Converts a yaml field to a Structure object - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Structure - */ - 'toStructure' => function (Field $field) { - try { - return new Structure(Data::decode($field->value, 'yaml'), $field->parent()); - } catch (Exception $e) { - if ($field->parent() === null) { - $message = 'Invalid structure data for "' . $field->key() . '" field'; - } else { - $message = 'Invalid structure data for "' . $field->key() . '" field on parent "' . $field->parent()->title() . '"'; - } + /** + * Converts a yaml field to a Structure object + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Structure + */ + 'toStructure' => function (Field $field) { + try { + return new Structure(Data::decode($field->value, 'yaml'), $field->parent()); + } catch (Exception $e) { + if ($field->parent() === null) { + $message = 'Invalid structure data for "' . $field->key() . '" field'; + } else { + $message = 'Invalid structure data for "' . $field->key() . '" field on parent "' . $field->parent()->title() . '"'; + } - throw new InvalidArgumentException($message); - } - }, + throw new InvalidArgumentException($message); + } + }, - /** - * Converts the field value to a Unix timestamp - * - * @param \Kirby\Cms\Field $field - * @return int - */ - 'toTimestamp' => function (Field $field): int { - return strtotime($field->value); - }, + /** + * Converts the field value to a Unix timestamp + * + * @param \Kirby\Cms\Field $field + * @return int + */ + 'toTimestamp' => function (Field $field): int { + return strtotime($field->value); + }, - /** - * Turns the field value into an absolute Url - * - * @param \Kirby\Cms\Field $field - * @return string - */ - 'toUrl' => function (Field $field): string { - return Url::to($field->value); - }, + /** + * Turns the field value into an absolute Url + * + * @param \Kirby\Cms\Field $field + * @return string + */ + 'toUrl' => function (Field $field): string { + return Url::to($field->value); + }, - /** - * Converts a user email address to a user object - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\User|null - */ - 'toUser' => function (Field $field) { - return $field->toUsers()->first(); - }, + /** + * Converts a user email address to a user object + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\User|null + */ + 'toUser' => function (Field $field) { + return $field->toUsers()->first(); + }, - /** - * Returns a users collection from a yaml list of user email addresses in the field - * - * @param \Kirby\Cms\Field $field - * @param string $separator - * @return \Kirby\Cms\Users - */ - 'toUsers' => function (Field $field, string $separator = 'yaml') use ($app) { - return $app->users()->find(false, false, ...$field->toData($separator)); - }, + /** + * Returns a users collection from a yaml list of user email addresses in the field + * + * @param \Kirby\Cms\Field $field + * @param string $separator + * @return \Kirby\Cms\Users + */ + 'toUsers' => function (Field $field, string $separator = 'yaml') use ($app) { + return $app->users()->find(false, false, ...$field->toData($separator)); + }, - // inspectors + // inspectors - /** - * Returns the length of the field content - */ - 'length' => function (Field $field) { - return Str::length($field->value); - }, + /** + * Returns the length of the field content + */ + 'length' => function (Field $field) { + return Str::length($field->value); + }, - /** - * Returns the number of words in the text - */ - 'words' => function (Field $field) { - return str_word_count(strip_tags($field->value)); - }, + /** + * Returns the number of words in the text + */ + 'words' => function (Field $field) { + return str_word_count(strip_tags($field->value)); + }, - // manipulators + // manipulators - /** - * Applies the callback function to the field - * @since 3.4.0 - * - * @param \Kirby\Cms\Field $field - * @param Closure $callback - */ - 'callback' => function (Field $field, Closure $callback) { - return $callback($field); - }, + /** + * Applies the callback function to the field + * @since 3.4.0 + * + * @param \Kirby\Cms\Field $field + * @param Closure $callback + */ + 'callback' => function (Field $field, Closure $callback) { + return $callback($field); + }, - /** - * Escapes the field value to be safely used in HTML - * templates without the risk of XSS attacks - * - * @param \Kirby\Cms\Field $field - * @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`) - */ - 'escape' => function (Field $field, string $context = 'html') { - $field->value = esc($field->value, $context); - return $field; - }, + /** + * Escapes the field value to be safely used in HTML + * templates without the risk of XSS attacks + * + * @param \Kirby\Cms\Field $field + * @param string $context Location of output (`html`, `attr`, `js`, `css`, `url` or `xml`) + */ + 'escape' => function (Field $field, string $context = 'html') { + $field->value = Str::esc($field->value, $context); + return $field; + }, - /** - * Creates an excerpt of the field value without html - * or any other formatting. - * - * @param \Kirby\Cms\Field $field - * @param int $cahrs - * @param bool $strip - * @param string $rep - * @return \Kirby\Cms\Field - */ - 'excerpt' => function (Field $field, int $chars = 0, bool $strip = true, string $rep = ' …') { - $field->value = Str::excerpt($field->kirbytext()->value(), $chars, $strip, $rep); - return $field; - }, + /** + * Creates an excerpt of the field value without html + * or any other formatting. + * + * @param \Kirby\Cms\Field $field + * @param int $cahrs + * @param bool $strip + * @param string $rep + * @return \Kirby\Cms\Field + */ + 'excerpt' => function (Field $field, int $chars = 0, bool $strip = true, string $rep = ' …') { + $field->value = Str::excerpt($field->kirbytext()->value(), $chars, $strip, $rep); + return $field; + }, - /** - * Converts the field content to valid HTML - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'html' => function (Field $field) { - $field->value = Html::encode($field->value); - return $field; - }, + /** + * Converts the field content to valid HTML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'html' => function (Field $field) { + $field->value = Html::encode($field->value); + return $field; + }, - /** - * Strips all block-level HTML elements from the field value, - * it can be safely placed inside of other inline elements - * without the risk of breaking the HTML structure. - * @since 3.3.0 - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'inline' => function (Field $field) { - // List of valid inline elements taken from: https://developer.mozilla.org/de/docs/Web/HTML/Inline_elemente - // Obsolete elements, script tags, image maps and form elements have - // been excluded for safety reasons and as they are most likely not - // needed in most cases. - $field->value = strip_tags($field->value, Html::$inlineList); - return $field; - }, + /** + * Strips all block-level HTML elements from the field value, + * it can be safely placed inside of other inline elements + * without the risk of breaking the HTML structure. + * @since 3.3.0 + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'inline' => function (Field $field) { + // List of valid inline elements taken from: https://developer.mozilla.org/de/docs/Web/HTML/Inline_elemente + // Obsolete elements, script tags, image maps and form elements have + // been excluded for safety reasons and as they are most likely not + // needed in most cases. + $field->value = strip_tags($field->value, Html::$inlineList); + return $field; + }, - /** - * Converts the field content from Markdown/Kirbytext to valid HTML - * - * @param \Kirby\Cms\Field $field - * @param array $options - * @return \Kirby\Cms\Field - */ - 'kirbytext' => function (Field $field, array $options = []) use ($app) { - $field->value = $app->kirbytext($field->value, A::merge($options, [ - 'parent' => $field->parent(), - 'field' => $field - ])); + /** + * Converts the field content from Markdown/Kirbytext to valid HTML + * + * @param \Kirby\Cms\Field $field + * @param array $options + * @return \Kirby\Cms\Field + */ + 'kirbytext' => function (Field $field, array $options = []) use ($app) { + $field->value = $app->kirbytext($field->value, A::merge($options, [ + 'parent' => $field->parent(), + 'field' => $field + ])); - return $field; - }, + return $field; + }, - /** - * Converts the field content from inline Markdown/Kirbytext - * to valid HTML - * @since 3.1.0 - * - * @param \Kirby\Cms\Field $field - * @param array $options - * @return \Kirby\Cms\Field - */ - 'kirbytextinline' => function (Field $field, array $options = []) use ($app) { - $field->value = $app->kirbytext($field->value, A::merge($options, [ - 'parent' => $field->parent(), - 'field' => $field, - 'markdown' => [ - 'inline' => true - ] - ])); + /** + * Converts the field content from inline Markdown/Kirbytext + * to valid HTML + * @since 3.1.0 + * + * @param \Kirby\Cms\Field $field + * @param array $options + * @return \Kirby\Cms\Field + */ + 'kirbytextinline' => function (Field $field, array $options = []) use ($app) { + $field->value = $app->kirbytext($field->value, A::merge($options, [ + 'parent' => $field->parent(), + 'field' => $field, + 'markdown' => [ + 'inline' => true + ] + ])); - return $field; - }, + return $field; + }, - /** - * Parses all KirbyTags without also parsing Markdown - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'kirbytags' => function (Field $field) use ($app) { - $field->value = $app->kirbytags($field->value, [ - 'parent' => $field->parent(), - 'field' => $field - ]); + /** + * Parses all KirbyTags without also parsing Markdown + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'kirbytags' => function (Field $field) use ($app) { + $field->value = $app->kirbytags($field->value, [ + 'parent' => $field->parent(), + 'field' => $field + ]); - return $field; - }, + return $field; + }, - /** - * Converts the field content to lowercase - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'lower' => function (Field $field) { - $field->value = Str::lower($field->value); - return $field; - }, + /** + * Converts the field content to lowercase + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'lower' => function (Field $field) { + $field->value = Str::lower($field->value); + return $field; + }, - /** - * Converts markdown to valid HTML - * - * @param \Kirby\Cms\Field $field - * @param array $options - * @return \Kirby\Cms\Field - */ - 'markdown' => function (Field $field, array $options = []) use ($app) { - $field->value = $app->markdown($field->value, $options); - return $field; - }, + /** + * Converts markdown to valid HTML + * + * @param \Kirby\Cms\Field $field + * @param array $options + * @return \Kirby\Cms\Field + */ + 'markdown' => function (Field $field, array $options = []) use ($app) { + $field->value = $app->markdown($field->value, $options); + return $field; + }, - /** - * Converts all line breaks in the field content to `
` tags. - * @since 3.3.0 - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'nl2br' => function (Field $field) { - $field->value = nl2br($field->value, false); - return $field; - }, + /** + * Converts all line breaks in the field content to `
` tags. + * @since 3.3.0 + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'nl2br' => function (Field $field) { + $field->value = nl2br($field->value, false); + return $field; + }, - /** - * Uses the field value as Kirby query - * - * @param \Kirby\Cms\Field $field - * @param string|null $expect - * @return mixed - */ - 'query' => function (Field $field, string $expect = null) use ($app) { - if ($parent = $field->parent()) { - return $parent->query($field->value, $expect); - } + /** + * Uses the field value as Kirby query + * + * @param \Kirby\Cms\Field $field + * @param string|null $expect + * @return mixed + */ + 'query' => function (Field $field, string $expect = null) use ($app) { + if ($parent = $field->parent()) { + return $parent->query($field->value, $expect); + } - return Str::query($field->value, [ - 'kirby' => $app, - 'site' => $app->site(), - 'page' => $app->page() - ]); - }, + return Str::query($field->value, [ + 'kirby' => $app, + 'site' => $app->site(), + 'page' => $app->page() + ]); + }, - /** - * It parses any queries found in the field value. - * - * @param \Kirby\Cms\Field $field - * @param array $data - * @param string $fallback Fallback for tokens in the template that cannot be replaced - * @return \Kirby\Cms\Field - */ - 'replace' => function (Field $field, array $data = [], string $fallback = '') use ($app) { - if ($parent = $field->parent()) { - // never pass `null` as the $template to avoid the fallback to the model ID - $field->value = $parent->toString($field->value ?? '', $data, $fallback); - } else { - $field->value = Str::template($field->value, array_replace([ - 'kirby' => $app, - 'site' => $app->site(), - 'page' => $app->page() - ], $data), ['fallback' => $fallback]); - } + /** + * It parses any queries found in the field value. + * + * @param \Kirby\Cms\Field $field + * @param array $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @return \Kirby\Cms\Field + */ + 'replace' => function (Field $field, array $data = [], string $fallback = '') use ($app) { + if ($parent = $field->parent()) { + // never pass `null` as the $template to avoid the fallback to the model ID + $field->value = $parent->toString($field->value ?? '', $data, $fallback); + } else { + $field->value = Str::template($field->value, array_replace([ + 'kirby' => $app, + 'site' => $app->site(), + 'page' => $app->page() + ], $data), ['fallback' => $fallback]); + } - return $field; - }, + return $field; + }, - /** - * Cuts the string after the given length and - * adds "…" if it is longer - * - * @param \Kirby\Cms\Field $field - * @param int $length The number of characters in the string - * @param string $appendix An optional replacement for the missing rest - * @return \Kirby\Cms\Field - */ - 'short' => function (Field $field, int $length, string $appendix = '…') { - $field->value = Str::short($field->value, $length, $appendix); - return $field; - }, + /** + * Cuts the string after the given length and + * adds "…" if it is longer + * + * @param \Kirby\Cms\Field $field + * @param int $length The number of characters in the string + * @param string $appendix An optional replacement for the missing rest + * @return \Kirby\Cms\Field + */ + 'short' => function (Field $field, int $length, string $appendix = '…') { + $field->value = Str::short($field->value, $length, $appendix); + return $field; + }, - /** - * Converts the field content to a slug - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'slug' => function (Field $field) { - $field->value = Str::slug($field->value); - return $field; - }, + /** + * Converts the field content to a slug + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'slug' => function (Field $field) { + $field->value = Str::slug($field->value); + return $field; + }, - /** - * Applies SmartyPants to the field - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'smartypants' => function (Field $field) use ($app) { - $field->value = $app->smartypants($field->value); - return $field; - }, + /** + * Applies SmartyPants to the field + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'smartypants' => function (Field $field) use ($app) { + $field->value = $app->smartypants($field->value); + return $field; + }, - /** - * Splits the field content into an array - * - * @param \Kirby\Cms\Field $field - * @return array - */ - 'split' => function (Field $field, $separator = ',') { - return Str::split((string)$field->value, $separator); - }, + /** + * Splits the field content into an array + * + * @param \Kirby\Cms\Field $field + * @return array + */ + 'split' => function (Field $field, $separator = ',') { + return Str::split((string)$field->value, $separator); + }, - /** - * Converts the field content to uppercase - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'upper' => function (Field $field) { - $field->value = Str::upper($field->value); - return $field; - }, + /** + * Converts the field content to uppercase + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'upper' => function (Field $field) { + $field->value = Str::upper($field->value); + return $field; + }, - /** - * Avoids typographical widows in strings by replacing - * the last space with ` ` - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'widont' => function (Field $field) { - $field->value = Str::widont($field->value); - return $field; - }, + /** + * Avoids typographical widows in strings by replacing + * the last space with ` ` + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'widont' => function (Field $field) { + $field->value = Str::widont($field->value); + return $field; + }, - /** - * Converts the field content to valid XML - * - * @param \Kirby\Cms\Field $field - * @return \Kirby\Cms\Field - */ - 'xml' => function (Field $field) { - $field->value = Xml::encode($field->value); - return $field; - }, + /** + * Converts the field content to valid XML + * + * @param \Kirby\Cms\Field $field + * @return \Kirby\Cms\Field + */ + 'xml' => function (Field $field) { + $field->value = Xml::encode($field->value); + return $field; + }, - // aliases + // aliases - /** - * Parses yaml in the field content and returns an array - * - * @param \Kirby\Cms\Field $field - * @return array - */ - 'yaml' => function (Field $field): array { - return $field->toData('yaml'); - }, + /** + * Parses yaml in the field content and returns an array + * + * @param \Kirby\Cms\Field $field + * @return array + */ + 'yaml' => function (Field $field): array { + return $field->toData('yaml'); + }, - ]; + ]; }; diff --git a/kirby/config/presets/files.php b/kirby/config/presets/files.php index fe0a7e5..c5cea1d 100644 --- a/kirby/config/presets/files.php +++ b/kirby/config/presets/files.php @@ -1,24 +1,26 @@ [ - 'headline' => $props['headline'] ?? t('files'), - 'type' => 'files', - 'layout' => $props['layout'] ?? 'cards', - 'template' => $props['template'] ?? null, - 'image' => $props['image'] ?? null, - 'info' => '{{ file.dimensions }}' - ] - ]; + $props['sections'] = [ + 'files' => [ + 'headline' => $props['headline'] ?? I18n::translate('files'), + 'type' => 'files', + 'layout' => $props['layout'] ?? 'cards', + 'template' => $props['template'] ?? null, + 'image' => $props['image'] ?? null, + 'info' => '{{ file.dimensions }}' + ] + ]; - // remove global options - unset( - $props['headline'], - $props['layout'], - $props['template'], - $props['image'] - ); + // remove global options + unset( + $props['headline'], + $props['layout'], + $props['template'], + $props['image'] + ); - return $props; + return $props; }; diff --git a/kirby/config/presets/page.php b/kirby/config/presets/page.php index 62df5de..fa0e6e6 100644 --- a/kirby/config/presets/page.php +++ b/kirby/config/presets/page.php @@ -1,72 +1,74 @@ $props - ]; - } + if (is_string($props) === true) { + $props = [ + 'headline' => $props + ]; + } - return array_replace_recursive($defaults, $props); - }; + return array_replace_recursive($defaults, $props); + }; - if (empty($props['sidebar']) === false) { - $sidebar = $props['sidebar']; - } else { - $sidebar = []; + if (empty($props['sidebar']) === false) { + $sidebar = $props['sidebar']; + } else { + $sidebar = []; - $pages = $props['pages'] ?? []; - $files = $props['files'] ?? []; + $pages = $props['pages'] ?? []; + $files = $props['files'] ?? []; - if ($pages !== false) { - $sidebar['pages'] = $section([ - 'headline' => t('pages'), - 'type' => 'pages', - 'status' => 'all', - 'layout' => 'list', - ], $pages); - } + if ($pages !== false) { + $sidebar['pages'] = $section([ + 'headline' => I18n::translate('pages'), + 'type' => 'pages', + 'status' => 'all', + 'layout' => 'list', + ], $pages); + } - if ($files !== false) { - $sidebar['files'] = $section([ - 'headline' => t('files'), - 'type' => 'files', - 'layout' => 'list' - ], $files); - } - } + if ($files !== false) { + $sidebar['files'] = $section([ + 'headline' => I18n::translate('files'), + 'type' => 'files', + 'layout' => 'list' + ], $files); + } + } - if (empty($sidebar) === true) { - $props['fields'] = $props['fields'] ?? []; + if (empty($sidebar) === true) { + $props['fields'] = $props['fields'] ?? []; - unset( - $props['files'], - $props['pages'] - ); - } else { - $props['columns'] = [ - [ - 'width' => '2/3', - 'fields' => $props['fields'] ?? [] - ], - [ - 'width' => '1/3', - 'sections' => $sidebar - ], - ]; + unset( + $props['files'], + $props['pages'] + ); + } else { + $props['columns'] = [ + [ + 'width' => '2/3', + 'fields' => $props['fields'] ?? [] + ], + [ + 'width' => '1/3', + 'sections' => $sidebar + ], + ]; - unset( - $props['fields'], - $props['files'], - $props['pages'], - $props['sidebar'] - ); - } + unset( + $props['fields'], + $props['files'], + $props['pages'], + $props['sidebar'] + ); + } - return $props; + return $props; }; diff --git a/kirby/config/presets/pages.php b/kirby/config/presets/pages.php index 5bba76b..8a3e51f 100644 --- a/kirby/config/presets/pages.php +++ b/kirby/config/presets/pages.php @@ -1,57 +1,58 @@ $headline, + 'type' => 'pages', + 'layout' => 'list', + 'status' => $status + ]; - $section = function ($headline, $status, $props) use ($templates) { - $defaults = [ - 'headline' => $headline, - 'type' => 'pages', - 'layout' => 'list', - 'status' => $status - ]; + if ($props === true) { + $props = []; + } - if ($props === true) { - $props = []; - } + if (is_string($props) === true) { + $props = [ + 'headline' => $props + ]; + } - if (is_string($props) === true) { - $props = [ - 'headline' => $props - ]; - } + // inject the global templates definition + if (empty($templates) === false) { + $props['templates'] = $props['templates'] ?? $templates; + } - // inject the global templates definition - if (empty($templates) === false) { - $props['templates'] = $props['templates'] ?? $templates; - } + return array_replace_recursive($defaults, $props); + }; - return array_replace_recursive($defaults, $props); - }; + $sections = []; - $sections = []; - - $drafts = $props['drafts'] ?? []; - $unlisted = $props['unlisted'] ?? false; - $listed = $props['listed'] ?? []; + $drafts = $props['drafts'] ?? []; + $unlisted = $props['unlisted'] ?? false; + $listed = $props['listed'] ?? []; - if ($drafts !== false) { - $sections['drafts'] = $section(t('pages.status.draft'), 'drafts', $drafts); - } + if ($drafts !== false) { + $sections['drafts'] = $section(I18n::translate('pages.status.draft'), 'drafts', $drafts); + } - if ($unlisted !== false) { - $sections['unlisted'] = $section(t('pages.status.unlisted'), 'unlisted', $unlisted); - } + if ($unlisted !== false) { + $sections['unlisted'] = $section(I18n::translate('pages.status.unlisted'), 'unlisted', $unlisted); + } - if ($listed !== false) { - $sections['listed'] = $section(t('pages.status.listed'), 'listed', $listed); - } + if ($listed !== false) { + $sections['listed'] = $section(I18n::translate('pages.status.listed'), 'listed', $listed); + } - // cleaning up - unset($props['drafts'], $props['unlisted'], $props['listed'], $props['templates']); + // cleaning up + unset($props['drafts'], $props['unlisted'], $props['listed'], $props['templates']); - return array_merge($props, ['sections' => $sections]); + return array_merge($props, ['sections' => $sections]); }; diff --git a/kirby/config/routes.php b/kirby/config/routes.php index 2c55031..3168dd3 100644 --- a/kirby/config/routes.php +++ b/kirby/config/routes.php @@ -8,141 +8,140 @@ use Kirby\Panel\Plugins; use Kirby\Toolkit\Str; return function ($kirby) { - $api = $kirby->option('api.slug', 'api'); - $panel = $kirby->option('panel.slug', 'panel'); - $index = $kirby->url('index'); - $media = $kirby->url('media'); + $api = $kirby->option('api.slug', 'api'); + $panel = $kirby->option('panel.slug', 'panel'); + $index = $kirby->url('index'); + $media = $kirby->url('media'); - if (Str::startsWith($media, $index) === true) { - $media = Str::after($media, $index); - } else { - // media URL is outside of the site, we can't make routing work; - // fall back to the standard media route - $media = 'media'; - } + if (Str::startsWith($media, $index) === true) { + $media = Str::after($media, $index); + } else { + // media URL is outside of the site, we can't make routing work; + // fall back to the standard media route + $media = 'media'; + } - /** - * Before routes are running before the - * plugin routes and cannot be overwritten by - * plugins. - */ - $before = [ - [ - 'pattern' => $api . '/(:all)', - 'method' => 'ALL', - 'env' => 'api', - 'action' => function ($path = null) use ($kirby) { - if ($kirby->option('api') === false) { - return null; - } + /** + * Before routes are running before the + * plugin routes and cannot be overwritten by + * plugins. + */ + $before = [ + [ + 'pattern' => $api . '/(:all)', + 'method' => 'ALL', + 'env' => 'api', + 'action' => function ($path = null) use ($kirby) { + if ($kirby->option('api') === false) { + return null; + } - $request = $kirby->request(); + $request = $kirby->request(); - return $kirby->api()->render($path, $this->method(), [ - 'body' => $request->body()->toArray(), - 'files' => $request->files()->toArray(), - 'headers' => $request->headers(), - 'query' => $request->query()->toArray(), - ]); - } - ], - [ - 'pattern' => $media . '/plugins/index.(css|js)', - 'env' => 'media', - 'action' => function (string $type) use ($kirby) { - $plugins = new Plugins(); + return $kirby->api()->render($path, $this->method(), [ + 'body' => $request->body()->toArray(), + 'files' => $request->files()->toArray(), + 'headers' => $request->headers(), + 'query' => $request->query()->toArray(), + ]); + } + ], + [ + 'pattern' => $media . '/plugins/index.(css|js)', + 'env' => 'media', + 'action' => function (string $type) use ($kirby) { + $plugins = new Plugins(); - return $kirby - ->response() - ->type($type) - ->body($plugins->read($type)); - } - ], - [ - 'pattern' => $media . '/plugins/(:any)/(:any)/(:all).(css|map|gif|js|mjs|jpg|png|svg|webp|avif|woff2|woff|json)', - 'env' => 'media', - 'action' => function (string $provider, string $pluginName, string $filename, string $extension) { - return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension); - } - ], - [ - 'pattern' => $media . '/pages/(:all)/(:any)/(:any)', - 'env' => 'media', - 'action' => function ($path, $hash, $filename) use ($kirby) { - return Media::link($kirby->page($path), $hash, $filename); - } - ], - [ - 'pattern' => $media . '/site/(:any)/(:any)', - 'env' => 'media', - 'action' => function ($hash, $filename) use ($kirby) { - return Media::link($kirby->site(), $hash, $filename); - } - ], - [ - 'pattern' => $media . '/users/(:any)/(:any)/(:any)', - 'env' => 'media', - 'action' => function ($id, $hash, $filename) use ($kirby) { - return Media::link($kirby->user($id), $hash, $filename); - } - ], - [ - 'pattern' => $media . '/assets/(:all)/(:any)/(:any)', - 'env' => 'media', - 'action' => function ($path, $hash, $filename) { - return Media::thumb($path, $hash, $filename); - } - ], - [ - 'pattern' => $panel . '/(:all?)', - 'method' => 'ALL', - 'env' => 'panel', - 'action' => function ($path = null) { - return Panel::router($path); - } - ], - ]; + return $kirby + ->response() + ->type($type) + ->body($plugins->read($type)); + } + ], + [ + 'pattern' => $media . '/plugins/(:any)/(:any)/(:all).(css|map|gif|js|mjs|jpg|png|svg|webp|avif|woff2|woff|json)', + 'env' => 'media', + 'action' => function (string $provider, string $pluginName, string $filename, string $extension) { + return PluginAssets::resolve($provider . '/' . $pluginName, $filename . '.' . $extension); + } + ], + [ + 'pattern' => $media . '/pages/(:all)/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($path, $hash, $filename) use ($kirby) { + return Media::link($kirby->page($path), $hash, $filename); + } + ], + [ + 'pattern' => $media . '/site/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($hash, $filename) use ($kirby) { + return Media::link($kirby->site(), $hash, $filename); + } + ], + [ + 'pattern' => $media . '/users/(:any)/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($id, $hash, $filename) use ($kirby) { + return Media::link($kirby->user($id), $hash, $filename); + } + ], + [ + 'pattern' => $media . '/assets/(:all)/(:any)/(:any)', + 'env' => 'media', + 'action' => function ($path, $hash, $filename) { + return Media::thumb($path, $hash, $filename); + } + ], + [ + 'pattern' => $panel . '/(:all?)', + 'method' => 'ALL', + 'env' => 'panel', + 'action' => function ($path = null) { + return Panel::router($path); + } + ], + ]; - // Multi-language setup - if ($kirby->multilang() === true) { - $after = LanguageRoutes::create($kirby); - } else { + // Multi-language setup + if ($kirby->multilang() === true) { + $after = LanguageRoutes::create($kirby); + } else { + // Single-language home + $after[] = [ + 'pattern' => '', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + return $kirby->resolve(); + } + ]; - // Single-language home - $after[] = [ - 'pattern' => '', - 'method' => 'ALL', - 'env' => 'site', - 'action' => function () use ($kirby) { - return $kirby->resolve(); - } - ]; + // redirect the home page folder to the real homepage + $after[] = [ + 'pattern' => $kirby->option('home', 'home'), + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + return $kirby + ->response() + ->redirect($kirby->site()->url()); + } + ]; - // redirect the home page folder to the real homepage - $after[] = [ - 'pattern' => $kirby->option('home', 'home'), - 'method' => 'ALL', - 'env' => 'site', - 'action' => function () use ($kirby) { - return $kirby - ->response() - ->redirect($kirby->site()->url()); - } - ]; + // Single-language subpages + $after[] = [ + 'pattern' => '(:all)', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function (string $path) use ($kirby) { + return $kirby->resolve($path); + } + ]; + } - // Single-language subpages - $after[] = [ - 'pattern' => '(:all)', - 'method' => 'ALL', - 'env' => 'site', - 'action' => function (string $path) use ($kirby) { - return $kirby->resolve($path); - } - ]; - } - - return [ - 'before' => $before, - 'after' => $after - ]; + return [ + 'before' => $before, + 'after' => $after + ]; }; diff --git a/kirby/config/sections/fields.php b/kirby/config/sections/fields.php index 4cb6a46..38565e5 100644 --- a/kirby/config/sections/fields.php +++ b/kirby/config/sections/fields.php @@ -3,54 +3,55 @@ use Kirby\Form\Form; return [ - 'props' => [ - 'fields' => function (array $fields = []) { - return $fields; - } - ], - 'computed' => [ - 'form' => function () { - $fields = $this->fields; - $disabled = $this->model->permissions()->update() === false; - $content = $this->model->content()->toArray(); + 'props' => [ + 'fields' => function (array $fields = []) { + return $fields; + } + ], + 'computed' => [ + 'form' => function () { + $fields = $this->fields; + $disabled = $this->model->permissions()->update() === false; + $lang = $this->model->kirby()->languageCode(); + $content = $this->model->content($lang)->toArray(); - if ($disabled === true) { - foreach ($fields as $key => $props) { - $fields[$key]['disabled'] = true; - } - } + if ($disabled === true) { + foreach ($fields as $key => $props) { + $fields[$key]['disabled'] = true; + } + } - return new Form([ - 'fields' => $fields, - 'values' => $content, - 'model' => $this->model, - 'strict' => true - ]); - }, - 'fields' => function () { - $fields = $this->form->fields()->toArray(); + return new Form([ + 'fields' => $fields, + 'values' => $content, + 'model' => $this->model, + 'strict' => true + ]); + }, + 'fields' => function () { + $fields = $this->form->fields()->toArray(); - if (is_a($this->model, 'Kirby\Cms\Page') === true || is_a($this->model, 'Kirby\Cms\Site') === true) { - // the title should never be updated directly via - // fields section to avoid conflicts with the rename dialog - unset($fields['title']); - } + if (is_a($this->model, 'Kirby\Cms\Page') === true || is_a($this->model, 'Kirby\Cms\Site') === true) { + // the title should never be updated directly via + // fields section to avoid conflicts with the rename dialog + unset($fields['title']); + } - foreach ($fields as $index => $props) { - unset($fields[$index]['value']); - } + foreach ($fields as $index => $props) { + unset($fields[$index]['value']); + } - return $fields; - } - ], - 'methods' => [ - 'errors' => function () { - return $this->form->errors(); - } - ], - 'toArray' => function () { - return [ - 'fields' => $this->fields, - ]; - } + return $fields; + } + ], + 'methods' => [ + 'errors' => function () { + return $this->form->errors(); + } + ], + 'toArray' => function () { + return [ + 'fields' => $this->fields, + ]; + } ]; diff --git a/kirby/config/sections/files.php b/kirby/config/sections/files.php index ca23a2a..eeb5347 100644 --- a/kirby/config/sections/files.php +++ b/kirby/config/sections/files.php @@ -4,242 +4,203 @@ use Kirby\Cms\File; use Kirby\Toolkit\I18n; return [ - 'mixins' => [ - 'empty', - 'headline', - 'help', - 'layout', - 'min', - 'max', - 'pagination', - 'parent', - ], - 'props' => [ - /** - * Enables/disables reverse sorting - */ - 'flip' => function (bool $flip = false) { - return $flip; - }, - /** - * Image options to control the source and look of file previews - */ - 'image' => function ($image = null) { - return $image ?? []; - }, - /** - * Optional info text setup. Info text is shown on the right (lists, cardlets) or below (cards) the filename. - */ - 'info' => function ($info = null) { - return I18n::translate($info, $info); - }, - /** - * The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge` - */ - 'size' => function (string $size = 'auto') { - return $size; - }, - /** - * Enables/disables manual sorting - */ - 'sortable' => function (bool $sortable = true) { - return $sortable; - }, - /** - * Overwrites manual sorting and sorts by the given field and sorting direction (i.e. `filename desc`) - */ - 'sortBy' => function (string $sortBy = null) { - return $sortBy; - }, - /** - * Filters all files by template and also sets the template, which will be used for all uploads - */ - 'template' => function (string $template = null) { - return $template; - }, - /** - * Setup for the main text in the list or cards. By default this will display the filename. - */ - 'text' => function ($text = '{{ file.filename }}') { - return I18n::translate($text, $text); - } - ], - 'computed' => [ - 'accept' => function () { - if ($this->template) { - $file = new File([ - 'filename' => 'tmp', - 'parent' => $this->model(), - 'template' => $this->template - ]); + 'mixins' => [ + 'details', + 'empty', + 'headline', + 'help', + 'layout', + 'min', + 'max', + 'pagination', + 'parent', + 'search', + 'sort' + ], + 'props' => [ + /** + * Filters all files by template and also sets the template, which will be used for all uploads + */ + 'template' => function (string $template = null) { + return $template; + }, + /** + * Setup for the main text in the list or cards. By default this will display the filename. + */ + 'text' => function ($text = '{{ file.filename }}') { + return I18n::translate($text, $text); + } + ], + 'computed' => [ + 'accept' => function () { + if ($this->template) { + $file = new File([ + 'filename' => 'tmp', + 'parent' => $this->model(), + 'template' => $this->template + ]); - return $file->blueprint()->acceptMime(); - } + return $file->blueprint()->acceptMime(); + } - return null; - }, - 'parent' => function () { - return $this->parentModel(); - }, - 'files' => function () { - $files = $this->parent->files()->template($this->template); + return null; + }, + 'parent' => function () { + return $this->parentModel(); + }, + 'files' => function () { + $files = $this->parent->files()->template($this->template); - // filter out all protected files - $files = $files->filter('isReadable', true); + // filter out all protected files + $files = $files->filter('isReadable', true); - if ($this->sortBy) { - $files = $files->sort(...$files::sortArgs($this->sortBy)); - } else { - $files = $files->sorted(); - } + // search + if ($this->search === true && empty($this->searchterm()) === false) { + $files = $files->search($this->searchterm()); + } - // flip - if ($this->flip === true) { - $files = $files->flip(); - } + // sort + if ($this->sortBy) { + $files = $files->sort(...$files::sortArgs($this->sortBy)); + } else { + $files = $files->sorted(); + } - // apply the default pagination - $files = $files->paginate([ - 'page' => $this->page, - 'limit' => $this->limit, - 'method' => 'none' // the page is manually provided - ]); + // flip + if ($this->flip === true) { + $files = $files->flip(); + } - return $files; - }, - 'data' => function () { - $data = []; + // apply the default pagination + $files = $files->paginate([ + 'page' => $this->page, + 'limit' => $this->limit, + 'method' => 'none' // the page is manually provided + ]); - // the drag text needs to be absolute when the files come from - // a different parent model - $dragTextAbsolute = $this->model->is($this->parent) === false; + return $files; + }, + 'data' => function () { + $data = []; - foreach ($this->files as $file) { - $panel = $file->panel(); + // the drag text needs to be absolute when the files come from + // a different parent model + $dragTextAbsolute = $this->model->is($this->parent) === false; - $data[] = [ - 'dragText' => $panel->dragText('auto', $dragTextAbsolute), - 'extension' => $file->extension(), - 'filename' => $file->filename(), - 'id' => $file->id(), - 'image' => $panel->image($this->image, $this->layout), - 'info' => $file->toSafeString($this->info ?? false), - 'link' => $panel->url(true), - 'mime' => $file->mime(), - 'parent' => $file->parent()->panel()->path(), - 'template' => $file->template(), - 'text' => $file->toSafeString($this->text), - 'url' => $file->url(), - ]; - } + foreach ($this->files as $file) { + $panel = $file->panel(); - return $data; - }, - 'total' => function () { - return $this->files->pagination()->total(); - }, - 'errors' => function () { - $errors = []; + $item = [ + 'dragText' => $panel->dragText('auto', $dragTextAbsolute), + '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(), + 'template' => $file->template(), + 'text' => $file->toSafeString($this->text), + 'url' => $file->url(), + ]; - if ($this->validateMax() === false) { - $errors['max'] = I18n::template('error.section.files.max.' . I18n::form($this->max), [ - 'max' => $this->max, - 'section' => $this->headline - ]); - } + if ($this->layout === 'table') { + $item = $this->columnsValues($item, $file); + } - if ($this->validateMin() === false) { - $errors['min'] = I18n::template('error.section.files.min.' . I18n::form($this->min), [ - 'min' => $this->min, - 'section' => $this->headline - ]); - } + $data[] = $item; + } - if (empty($errors) === true) { - return []; - } + return $data; + }, + 'total' => function () { + return $this->files->pagination()->total(); + }, + 'errors' => function () { + $errors = []; - return [ - $this->name => [ - 'label' => $this->headline, - 'message' => $errors, - ] - ]; - }, - 'link' => function () { - $modelLink = $this->model->panel()->url(true); - $parentLink = $this->parent->panel()->url(true); + if ($this->validateMax() === false) { + $errors['max'] = I18n::template('error.section.files.max.' . I18n::form($this->max), [ + 'max' => $this->max, + 'section' => $this->headline + ]); + } - if ($modelLink !== $parentLink) { - return $parentLink; - } - }, - 'pagination' => function () { - return $this->pagination(); - }, - 'sortable' => function () { - if ($this->sortable === false) { - return false; - } + if ($this->validateMin() === false) { + $errors['min'] = I18n::template('error.section.files.min.' . I18n::form($this->min), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } - if ($this->sortBy !== null) { - return false; - } + if (empty($errors) === true) { + return []; + } - if ($this->flip === true) { - return false; - } + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'pagination' => function () { + return $this->pagination(); + }, + 'upload' => function () { + if ($this->isFull() === true) { + return false; + } - return true; - }, - 'upload' => function () { - if ($this->isFull() === true) { - return false; - } + // count all uploaded files + $total = count($this->data); + $max = $this->max ? $this->max - $total : null; - // count all uploaded files - $total = count($this->data); - $max = $this->max ? $this->max - $total : null; + if ($this->max && $total === $this->max - 1) { + $multiple = false; + } else { + $multiple = true; + } - if ($this->max && $total === $this->max - 1) { - $multiple = false; - } else { - $multiple = true; - } + $template = $this->template === 'default' ? null : $this->template; - $template = $this->template === 'default' ? null : $this->template; - - return [ - 'accept' => $this->accept, - 'multiple' => $multiple, - 'max' => $max, - 'api' => $this->parent->apiUrl(true) . '/files', - 'attributes' => array_filter([ - 'sort' => $this->sortable === true ? $total + 1 : null, - 'template' => $template - ]) - ]; - } - ], - 'toArray' => function () { - return [ - 'data' => $this->data, - 'errors' => $this->errors, - 'options' => [ - 'accept' => $this->accept, - 'apiUrl' => $this->parent->apiUrl(true), - 'empty' => $this->empty, - 'headline' => $this->headline, - 'help' => $this->help, - 'layout' => $this->layout, - 'link' => $this->link, - 'max' => $this->max, - 'min' => $this->min, - 'size' => $this->size, - 'sortable' => $this->sortable, - 'upload' => $this->upload - ], - 'pagination' => $this->pagination - ]; - } + return [ + 'accept' => $this->accept, + 'multiple' => $multiple, + 'max' => $max, + 'api' => $this->parent->apiUrl(true) . '/files', + 'attributes' => array_filter([ + 'sort' => $this->sortable === true ? $total + 1 : null, + 'template' => $template + ]) + ]; + } + ], + 'toArray' => function () { + return [ + 'data' => $this->data, + 'errors' => $this->errors, + 'options' => [ + 'accept' => $this->accept, + 'apiUrl' => $this->parent->apiUrl(true), + 'columns' => $this->columns, + 'empty' => $this->empty, + 'headline' => $this->headline, + 'help' => $this->help, + 'layout' => $this->layout, + 'link' => $this->link(), + 'max' => $this->max, + 'min' => $this->min, + 'search' => $this->search, + 'size' => $this->size, + 'sortable' => $this->sortable, + 'upload' => $this->upload + ], + 'pagination' => $this->pagination + ]; + } ]; diff --git a/kirby/config/sections/info.php b/kirby/config/sections/info.php index 555af89..e348bd4 100644 --- a/kirby/config/sections/info.php +++ b/kirby/config/sections/info.php @@ -3,31 +3,31 @@ use Kirby\Toolkit\I18n; return [ - 'mixins' => [ - 'headline', - ], - 'props' => [ - 'text' => function ($text = null) { - return I18n::translate($text, $text); - }, - 'theme' => function (string $theme = null) { - return $theme; - } - ], - 'computed' => [ - 'text' => function () { - if ($this->text) { - $text = $this->model()->toSafeString($this->text); - $text = $this->kirby()->kirbytext($text); - return $text; - } - }, - ], - 'toArray' => function () { - return [ - 'headline' => $this->headline, - 'text' => $this->text, - 'theme' => $this->theme - ]; - } + 'mixins' => [ + 'headline', + ], + 'props' => [ + 'text' => function ($text = null) { + return I18n::translate($text, $text); + }, + 'theme' => function (string $theme = null) { + return $theme; + } + ], + 'computed' => [ + 'text' => function () { + if ($this->text) { + $text = $this->model()->toSafeString($this->text); + $text = $this->kirby()->kirbytext($text); + return $text; + } + }, + ], + 'toArray' => function () { + return [ + 'headline' => $this->headline, + 'text' => $this->text, + 'theme' => $this->theme + ]; + } ]; diff --git a/kirby/config/sections/mixins/details.php b/kirby/config/sections/mixins/details.php new file mode 100644 index 0000000..3d2c928 --- /dev/null +++ b/kirby/config/sections/mixins/details.php @@ -0,0 +1,36 @@ + [ + /** + * Image options to control the source and look of preview + */ + 'image' => function ($image = null) { + return $image ?? []; + }, + /** + * Optional info text setup. Info text is shown on the right (lists, cardlets) or below (cards) the title. + */ + 'info' => function ($info = null) { + return I18n::translate($info, $info); + }, + /** + * Setup for the main text in the list or cards. By default this will display the title. + */ + 'text' => function ($text = '{{ model.title }}') { + return I18n::translate($text, $text); + } + ], + 'methods' => [ + 'link' => function () { + $modelLink = $this->model->panel()->url(true); + $parentLink = $this->parent->panel()->url(true); + + if ($modelLink !== $parentLink) { + return $parentLink; + } + } + ] +]; diff --git a/kirby/config/sections/mixins/empty.php b/kirby/config/sections/mixins/empty.php index 967b252..97c2404 100644 --- a/kirby/config/sections/mixins/empty.php +++ b/kirby/config/sections/mixins/empty.php @@ -3,19 +3,19 @@ use Kirby\Toolkit\I18n; return [ - 'props' => [ - /** - * Sets the text for the empty state box - */ - 'empty' => function ($empty = null) { - return I18n::translate($empty, $empty); - } - ], - 'computed' => [ - 'empty' => function () { - if ($this->empty) { - return $this->model()->toSafeString($this->empty); - } - } - ] + 'props' => [ + /** + * Sets the text for the empty state box + */ + 'empty' => function ($empty = null) { + return I18n::translate($empty, $empty); + } + ], + 'computed' => [ + 'empty' => function () { + if ($this->empty) { + return $this->model()->toSafeString($this->empty); + } + } + ] ]; diff --git a/kirby/config/sections/mixins/headline.php b/kirby/config/sections/mixins/headline.php index f4bb7e1..df323c6 100644 --- a/kirby/config/sections/mixins/headline.php +++ b/kirby/config/sections/mixins/headline.php @@ -1,23 +1,42 @@ [ - /** - * The headline for the section. This can be a simple string or a template with additional info from the parent page. - */ - 'headline' => function ($headline = null) { - return I18n::translate($headline, $headline); - } - ], - 'computed' => [ - 'headline' => function () { - if ($this->headline) { - return $this->model()->toString($this->headline); - } + 'props' => [ + /** + * The headline for the section. This can be a simple string or a template with additional info from the parent page. + * @todo remove in 3.9.0 + */ + 'headline' => function ($headline = null) { + // TODO: add deprecation notive in 3.8.0 + // if ($headline !== null) { + // Helpers::deprecated('`headline` prop for sections has been deprecated and will be removed in Kirby 3.9.0. Use `label` instead.'); + // } - return ucfirst($this->name); - } - ] + return I18n::translate($headline, $headline); + }, + /** + * The label for the section. This can be a simple string or + * a template with additional info from the parent page. + * Replaces the `headline` prop. + */ + 'label' => function ($label = null) { + return I18n::translate($label, $label); + } + ], + 'computed' => [ + 'headline' => function () { + if ($this->headline) { + return $this->model()->toString($this->headline); + } + + if ($this->label) { + return $this->model()->toString($this->label); + } + + return ucfirst($this->name); + } + ] ]; diff --git a/kirby/config/sections/mixins/help.php b/kirby/config/sections/mixins/help.php index 1619e32..c95db08 100644 --- a/kirby/config/sections/mixins/help.php +++ b/kirby/config/sections/mixins/help.php @@ -3,21 +3,21 @@ use Kirby\Toolkit\I18n; return [ - 'props' => [ - /** - * Sets the help text - */ - 'help' => function ($help = null) { - return I18n::translate($help, $help); - } - ], - 'computed' => [ - 'help' => function () { - if ($this->help) { - $help = $this->model()->toSafeString($this->help); - $help = $this->kirby()->kirbytext($help); - return $help; - } - } - ] + 'props' => [ + /** + * Sets the help text + */ + 'help' => function ($help = null) { + return I18n::translate($help, $help); + } + ], + 'computed' => [ + 'help' => function () { + if ($this->help) { + $help = $this->model()->toSafeString($this->help); + $help = $this->kirby()->kirbytext($help); + return $help; + } + } + ] ]; diff --git a/kirby/config/sections/mixins/layout.php b/kirby/config/sections/mixins/layout.php index c545429..d362a7c 100644 --- a/kirby/config/sections/mixins/layout.php +++ b/kirby/config/sections/mixins/layout.php @@ -1,14 +1,129 @@ [ - /** - * Section layout. - * Available layout methods: `list`, `cardlets`, `cards`. - */ - 'layout' => function (string $layout = 'list') { - $layouts = ['list', 'cardlets', 'cards']; - return in_array($layout, $layouts) ? $layout : 'list'; - } - ] + 'props' => [ + /** + * Columns config for `layout: table` + */ + 'columns' => function (array $columns = null) { + return $columns ?? []; + }, + /** + * Section layout. + * Available layout methods: `list`, `cardlets`, `cards`, `table`. + */ + 'layout' => function (string $layout = 'list') { + $layouts = ['list', 'cardlets', 'cards', 'table']; + return in_array($layout, $layouts) ? $layout : 'list'; + }, + /** + * The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge` + */ + 'size' => function (string $size = 'auto') { + return $size; + }, + ], + 'computed' => [ + 'columns' => function () { + $columns = []; + + if ($this->layout !== 'table') { + return []; + } + + if ($this->image !== false) { + $columns['image'] = [ + 'label' => ' ', + 'mobile' => true, + 'type' => 'image', + 'width' => 'var(--table-row-height)' + ]; + } + + if ($this->text) { + $columns['title'] = [ + 'label' => I18n::translate('title'), + 'mobile' => true, + 'type' => 'url', + ]; + } + + if ($this->info) { + $columns['info'] = [ + 'label' => I18n::translate('info'), + 'type' => 'text', + ]; + } + + foreach ($this->columns as $columnName => $column) { + if ($column === true) { + $column = []; + } + + if ($column === false) { + continue; + } + + // fallback for labels + $column['label'] ??= Str::ucfirst($columnName); + + // make sure to translate labels + $column['label'] = I18n::translate($column['label'], $column['label']); + + // keep the original column name as id + $column['id'] = $columnName; + + // add the custom column to the array with a key that won't + // override the system columns + $columns[$columnName . 'Cell'] = $column; + } + + if ($this->type === 'pages') { + $columns['flag'] = [ + 'label' => ' ', + 'mobile' => true, + 'type' => 'flag', + 'width' => 'var(--table-row-height)', + ]; + } + + return $columns; + }, + ], + 'methods' => [ + 'columnsValues' => function (array $item, $model) { + $item['title'] = [ + // override toSafeString() coming from `$item` + // because the table cells don't use v-html + 'text' => $model->toString($this->text), + 'href' => $model->panel()->url(true) + ]; + + if ($this->info) { + // override toSafeString() coming from `$item` + // because the table cells don't use v-html + $item['info'] = $model->toString($this->info); + } + + foreach ($this->columns as $columnName => $column) { + // don't overwrite essential columns + if (isset($item[$columnName]) === true) { + continue; + } + + if (empty($column['value']) === false) { + $value = $model->toString($column['value']); + } else { + $value = $model->content()->get($column['id'] ?? $columnName)->value(); + } + + $item[$columnName] = $value; + } + + return $item; + } + ], ]; diff --git a/kirby/config/sections/mixins/max.php b/kirby/config/sections/mixins/max.php index 5ce303c..a87c1cc 100644 --- a/kirby/config/sections/mixins/max.php +++ b/kirby/config/sections/mixins/max.php @@ -1,28 +1,28 @@ [ - /** - * Sets the maximum number of allowed entries in the section - */ - 'max' => function (int $max = null) { - return $max; - } - ], - 'methods' => [ - 'isFull' => function () { - if ($this->max) { - return $this->total >= $this->max; - } + 'props' => [ + /** + * Sets the maximum number of allowed entries in the section + */ + 'max' => function (int $max = null) { + return $max; + } + ], + 'methods' => [ + 'isFull' => function () { + if ($this->max) { + return $this->total >= $this->max; + } - return false; - }, - 'validateMax' => function () { - if ($this->max && $this->total > $this->max) { - return false; - } + return false; + }, + 'validateMax' => function () { + if ($this->max && $this->total > $this->max) { + return false; + } - return true; - } - ] + return true; + } + ] ]; diff --git a/kirby/config/sections/mixins/min.php b/kirby/config/sections/mixins/min.php index bfc495d..6295f2d 100644 --- a/kirby/config/sections/mixins/min.php +++ b/kirby/config/sections/mixins/min.php @@ -1,21 +1,21 @@ [ - /** - * Sets the minimum number of required entries in the section - */ - 'min' => function (int $min = null) { - return $min; - } - ], - 'methods' => [ - 'validateMin' => function () { - if ($this->min && $this->min > $this->total) { - return false; - } + 'props' => [ + /** + * Sets the minimum number of required entries in the section + */ + 'min' => function (int $min = null) { + return $min; + } + ], + 'methods' => [ + 'validateMin' => function () { + if ($this->min && $this->min > $this->total) { + return false; + } - return true; - } - ] + return true; + } + ] ]; diff --git a/kirby/config/sections/mixins/pagination.php b/kirby/config/sections/mixins/pagination.php index 8bf3dee..3b2a2b0 100644 --- a/kirby/config/sections/mixins/pagination.php +++ b/kirby/config/sections/mixins/pagination.php @@ -1,36 +1,37 @@ [ - /** - * Sets the number of items per page. If there are more items the pagination navigation will be shown at the bottom of the section. - */ - 'limit' => function (int $limit = 20) { - return $limit; - }, - /** - * Sets the default page for the pagination. This will overwrite default pagination. - */ - 'page' => function (int $page = null) { - return get('page', $page); - }, - ], - 'methods' => [ - 'pagination' => function () { - $pagination = new Pagination([ - 'limit' => $this->limit, - 'page' => $this->page, - 'total' => $this->total - ]); + 'props' => [ + /** + * Sets the number of items per page. If there are more items the pagination navigation will be shown at the bottom of the section. + */ + 'limit' => function (int $limit = 20) { + return $limit; + }, + /** + * Sets the default page for the pagination. This will overwrite default pagination. + */ + 'page' => function (int $page = null) { + return App::instance()->request()->get('page', $page); + }, + ], + 'methods' => [ + 'pagination' => function () { + $pagination = new Pagination([ + 'limit' => $this->limit, + 'page' => $this->page, + 'total' => $this->total + ]); - return [ - 'limit' => $pagination->limit(), - 'offset' => $pagination->offset(), - 'page' => $pagination->page(), - 'total' => $pagination->total(), - ]; - }, - ] + return [ + 'limit' => $pagination->limit(), + 'offset' => $pagination->offset(), + 'page' => $pagination->page(), + 'total' => $pagination->total(), + ]; + } + ] ]; diff --git a/kirby/config/sections/mixins/parent.php b/kirby/config/sections/mixins/parent.php index 3534acf..2df8425 100644 --- a/kirby/config/sections/mixins/parent.php +++ b/kirby/config/sections/mixins/parent.php @@ -3,41 +3,41 @@ use Kirby\Exception\Exception; return [ - 'props' => [ - /** - * Sets the query to a parent to find items for the list - */ - 'parent' => function (string $parent = null) { - return $parent; - } - ], - 'methods' => [ - 'parentModel' => function () { - $parent = $this->parent; + 'props' => [ + /** + * Sets the query to a parent to find items for the list + */ + 'parent' => function (string $parent = null) { + return $parent; + } + ], + 'methods' => [ + 'parentModel' => function () { + $parent = $this->parent; - if (is_string($parent) === true) { - $query = $parent; - $parent = $this->model->query($query); + if (is_string($parent) === true) { + $query = $parent; + $parent = $this->model->query($query); - if (!$parent) { - throw new Exception('The parent for the query "' . $query . '" cannot be found in the section "' . $this->name() . '"'); - } + if (!$parent) { + throw new Exception('The parent for the query "' . $query . '" cannot be found in the section "' . $this->name() . '"'); + } - if ( - is_a($parent, 'Kirby\Cms\Page') === false && - is_a($parent, 'Kirby\Cms\Site') === false && - is_a($parent, 'Kirby\Cms\File') === false && - is_a($parent, 'Kirby\Cms\User') === false - ) { - throw new Exception('The parent for the section "' . $this->name() . '" has to be a page, site or user object'); - } - } + if ( + is_a($parent, 'Kirby\Cms\Page') === false && + is_a($parent, 'Kirby\Cms\Site') === false && + is_a($parent, 'Kirby\Cms\File') === false && + is_a($parent, 'Kirby\Cms\User') === false + ) { + throw new Exception('The parent for the section "' . $this->name() . '" has to be a page, site or user object'); + } + } - if ($parent === null) { - return $this->model; - } + if ($parent === null) { + return $this->model; + } - return $parent; - } - ] + return $parent; + } + ] ]; diff --git a/kirby/config/sections/mixins/search.php b/kirby/config/sections/mixins/search.php new file mode 100644 index 0000000..a50a375 --- /dev/null +++ b/kirby/config/sections/mixins/search.php @@ -0,0 +1,19 @@ + [ + /** + * Enable/disable the search in the sections + */ + 'search' => function (bool $search = false): bool { + return $search; + } + ], + 'methods' => [ + 'searchterm' => function (): ?string { + return App::instance()->request()->get('searchterm'); + } + ] +]; diff --git a/kirby/config/sections/mixins/sort.php b/kirby/config/sections/mixins/sort.php new file mode 100644 index 0000000..4387bae --- /dev/null +++ b/kirby/config/sections/mixins/sort.php @@ -0,0 +1,53 @@ + [ + /** + * Enables/disables reverse sorting + */ + 'flip' => function (bool $flip = false) { + return $flip; + }, + /** + * Enables/disables manual sorting + */ + 'sortable' => function (bool $sortable = true) { + return $sortable; + }, + /** + * Overwrites manual sorting and sorts by the given field and sorting direction (i.e. `date desc`) + */ + 'sortBy' => function (string $sortBy = null) { + return $sortBy; + }, + ], + 'computed' => [ + 'sortable' => function () { + if ($this->sortable === false) { + return false; + } + + if ( + $this->type === 'pages' && + in_array($this->status, ['listed', 'published', 'all']) === false + ) { + return false; + } + + // don't allow sorting while search filter is active + if (empty($this->searchterm()) === false) { + return false; + } + + if ($this->sortBy !== null) { + return false; + } + + if ($this->flip === true) { + return false; + } + + return true; + } + ] +]; diff --git a/kirby/config/sections/pages.php b/kirby/config/sections/pages.php index 541f7c0..35f666e 100644 --- a/kirby/config/sections/pages.php +++ b/kirby/config/sections/pages.php @@ -6,302 +6,258 @@ use Kirby\Toolkit\A; use Kirby\Toolkit\I18n; return [ - 'mixins' => [ - 'empty', - 'headline', - 'help', - 'layout', - 'min', - 'max', - 'pagination', - 'parent' - ], - 'props' => [ - /** - * Optional array of templates that should only be allowed to add - * or `false` to completely disable page creation - */ - 'create' => function ($create = null) { - return $create; - }, - /** - * Enables/disables reverse sorting - */ - 'flip' => function (bool $flip = false) { - return $flip; - }, - /** - * Image options to control the source and look of page previews - */ - 'image' => function ($image = null) { - return $image ?? []; - }, - /** - * Optional info text setup. Info text is shown on the right (lists) or below (cards) the page title. - */ - 'info' => function ($info = null) { - return I18n::translate($info, $info); - }, - /** - * The size option controls the size of cards. By default cards are auto-sized and the cards grid will always fill the full width. With a size you can disable auto-sizing. Available sizes: `tiny`, `small`, `medium`, `large`, `huge` - */ - 'size' => function (string $size = 'auto') { - return $size; - }, - /** - * Enables/disables manual sorting - */ - 'sortable' => function (bool $sortable = true) { - return $sortable; - }, - /** - * Overwrites manual sorting and sorts by the given field and sorting direction (i.e. `date desc`) - */ - 'sortBy' => function (string $sortBy = null) { - return $sortBy; - }, - /** - * Filters pages by their status. Available status settings: `draft`, `unlisted`, `listed`, `published`, `all`. - */ - 'status' => function (string $status = '') { - if ($status === 'drafts') { - $status = 'draft'; - } + 'mixins' => [ + 'details', + 'empty', + 'headline', + 'help', + 'layout', + 'min', + 'max', + 'pagination', + 'parent', + 'search', + 'sort' + ], + 'props' => [ + /** + * Optional array of templates that should only be allowed to add + * or `false` to completely disable page creation + */ + 'create' => function ($create = null) { + return $create; + }, + /** + * Filters pages by their status. Available status settings: `draft`, `unlisted`, `listed`, `published`, `all`. + */ + 'status' => function (string $status = '') { + if ($status === 'drafts') { + $status = 'draft'; + } - if (in_array($status, ['all', 'draft', 'published', 'listed', 'unlisted']) === false) { - $status = 'all'; - } + if (in_array($status, ['all', 'draft', 'published', 'listed', 'unlisted']) === false) { + $status = 'all'; + } - return $status; - }, - /** - * Filters the list by templates and sets template options when adding new pages to the section. - */ - 'templates' => function ($templates = null) { - return A::wrap($templates ?? $this->template); - }, - /** - * Setup for the main text in the list or cards. By default this will display the page title. - */ - 'text' => function ($text = '{{ page.title }}') { - return I18n::translate($text, $text); - } - ], - 'computed' => [ - 'parent' => function () { - $parent = $this->parentModel(); + return $status; + }, + /** + * Filters the list by templates and sets template options when adding new pages to the section. + */ + 'templates' => function ($templates = null) { + return A::wrap($templates ?? $this->template); + } + ], + 'computed' => [ + 'parent' => function () { + $parent = $this->parentModel(); - if (is_a($parent, 'Kirby\Cms\Site') === false && is_a($parent, 'Kirby\Cms\Page') === false) { - throw new InvalidArgumentException('The parent is invalid. You must choose the site or a page as parent.'); - } + if ( + is_a($parent, 'Kirby\Cms\Site') === false && + is_a($parent, 'Kirby\Cms\Page') === false + ) { + throw new InvalidArgumentException('The parent is invalid. You must choose the site or a page as parent.'); + } - return $parent; - }, - 'pages' => function () { - switch ($this->status) { - case 'draft': - $pages = $this->parent->drafts(); - break; - case 'listed': - $pages = $this->parent->children()->listed(); - break; - case 'published': - $pages = $this->parent->children(); - break; - case 'unlisted': - $pages = $this->parent->children()->unlisted(); - break; - default: - $pages = $this->parent->childrenAndDrafts(); - } + return $parent; + }, + 'pages' => function () { + switch ($this->status) { + case 'draft': + $pages = $this->parent->drafts(); + break; + case 'listed': + $pages = $this->parent->children()->listed(); + break; + case 'published': + $pages = $this->parent->children(); + break; + case 'unlisted': + $pages = $this->parent->children()->unlisted(); + break; + default: + $pages = $this->parent->childrenAndDrafts(); + } - // loop for the best performance - foreach ($pages->data as $id => $page) { + // 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 pages + if ($page->isReadable() === false) { + return false; + } - // remove all protected pages - if ($page->isReadable() === false) { - unset($pages->data[$id]); - continue; - } + // filter by all set templates + if ($this->templates && in_array($page->intendedTemplate()->name(), $this->templates) === false) { + return false; + } - // filter by all set templates - if ($this->templates && in_array($page->intendedTemplate()->name(), $this->templates) === false) { - unset($pages->data[$id]); - continue; - } - } + return true; + }); - // sort - if ($this->sortBy) { - $pages = $pages->sort(...$pages::sortArgs($this->sortBy)); - } + // search + if ($this->search === true && empty($this->searchterm()) === false) { + $pages = $pages->search($this->searchterm()); + } - // flip - if ($this->flip === true) { - $pages = $pages->flip(); - } + // sort + if ($this->sortBy) { + $pages = $pages->sort(...$pages::sortArgs($this->sortBy)); + } - // pagination - $pages = $pages->paginate([ - 'page' => $this->page, - 'limit' => $this->limit, - 'method' => 'none' // the page is manually provided - ]); + // flip + if ($this->flip === true) { + $pages = $pages->flip(); + } - return $pages; - }, - 'total' => function () { - return $this->pages->pagination()->total(); - }, - 'data' => function () { - $data = []; + // pagination + $pages = $pages->paginate([ + 'page' => $this->page, + 'limit' => $this->limit, + 'method' => 'none' // the page is manually provided + ]); - foreach ($this->pages as $item) { - $panel = $item->panel(); - $permissions = $item->permissions(); + return $pages; + }, + 'total' => function () { + return $this->pages->pagination()->total(); + }, + 'data' => function () { + $data = []; - $data[] = [ - 'dragText' => $panel->dragText(), - 'id' => $item->id(), - 'image' => $panel->image($this->image, $this->layout), - 'info' => $item->toSafeString($this->info ?? false), - 'link' => $panel->url(true), - 'parent' => $item->parentId(), - 'permissions' => [ - 'sort' => $permissions->can('sort'), - 'changeSlug' => $permissions->can('changeSlug'), - 'changeStatus' => $permissions->can('changeStatus'), - 'changeTitle' => $permissions->can('changeTitle'), - ], - 'status' => $item->status(), - 'template' => $item->intendedTemplate()->name(), - 'text' => $item->toSafeString($this->text), - ]; - } + foreach ($this->pages as $page) { + $panel = $page->panel(); + $permissions = $page->permissions(); - return $data; - }, - 'errors' => function () { - $errors = []; + $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' => [ + 'sort' => $permissions->can('sort'), + 'changeSlug' => $permissions->can('changeSlug'), + 'changeStatus' => $permissions->can('changeStatus'), + 'changeTitle' => $permissions->can('changeTitle'), + ], + 'status' => $page->status(), + 'template' => $page->intendedTemplate()->name(), + 'text' => $page->toSafeString($this->text), + ]; - if ($this->validateMax() === false) { - $errors['max'] = I18n::template('error.section.pages.max.' . I18n::form($this->max), [ - 'max' => $this->max, - 'section' => $this->headline - ]); - } + if ($this->layout === 'table') { + $item = $this->columnsValues($item, $page); + } - if ($this->validateMin() === false) { - $errors['min'] = I18n::template('error.section.pages.min.' . I18n::form($this->min), [ - 'min' => $this->min, - 'section' => $this->headline - ]); - } + $data[] = $item; + } - if (empty($errors) === true) { - return []; - } + return $data; + }, + 'errors' => function () { + $errors = []; - return [ - $this->name => [ - 'label' => $this->headline, - 'message' => $errors, - ] - ]; - }, - 'add' => function () { - if ($this->create === false) { - return false; - } + if ($this->validateMax() === false) { + $errors['max'] = I18n::template('error.section.pages.max.' . I18n::form($this->max), [ + 'max' => $this->max, + 'section' => $this->headline + ]); + } - if (in_array($this->status, ['draft', 'all']) === false) { - return false; - } + if ($this->validateMin() === false) { + $errors['min'] = I18n::template('error.section.pages.min.' . I18n::form($this->min), [ + 'min' => $this->min, + 'section' => $this->headline + ]); + } - if ($this->isFull() === true) { - return false; - } + if (empty($errors) === true) { + return []; + } - return true; - }, - 'link' => function () { - $modelLink = $this->model->panel()->url(true); - $parentLink = $this->parent->panel()->url(true); + return [ + $this->name => [ + 'label' => $this->headline, + 'message' => $errors, + ] + ]; + }, + 'add' => function () { + if ($this->create === false) { + return false; + } - if ($modelLink !== $parentLink) { - return $parentLink; - } - }, - 'pagination' => function () { - return $this->pagination(); - }, - 'sortable' => function () { - if (in_array($this->status, ['listed', 'published', 'all']) === false) { - return false; - } + if (in_array($this->status, ['draft', 'all']) === false) { + return false; + } - if ($this->sortable === false) { - return false; - } + if ($this->isFull() === true) { + return false; + } - if ($this->sortBy !== null) { - return false; - } + return true; + }, + 'pagination' => function () { + return $this->pagination(); + } + ], + 'methods' => [ + 'blueprints' => function () { + $blueprints = []; + $templates = empty($this->create) === false ? A::wrap($this->create) : $this->templates; - if ($this->flip === true) { - return false; - } + if (empty($templates) === true) { + $templates = $this->kirby()->blueprints(); + } - return true; - } - ], - 'methods' => [ - 'blueprints' => function () { - $blueprints = []; - $templates = empty($this->create) === false ? A::wrap($this->create) : $this->templates; + // convert every template to a usable option array + // for the template select box + foreach ($templates as $template) { + try { + $props = Blueprint::load('pages/' . $template); - if (empty($templates) === true) { - $templates = $this->kirby()->blueprints(); - } + $blueprints[] = [ + 'name' => basename($props['name']), + 'title' => $props['title'], + ]; + } catch (Throwable $e) { + $blueprints[] = [ + 'name' => basename($template), + 'title' => ucfirst($template), + ]; + } + } - // convert every template to a usable option array - // for the template select box - foreach ($templates as $template) { - try { - $props = Blueprint::load('pages/' . $template); - - $blueprints[] = [ - 'name' => basename($props['name']), - 'title' => $props['title'], - ]; - } catch (Throwable $e) { - $blueprints[] = [ - 'name' => basename($template), - 'title' => ucfirst($template), - ]; - } - } - - return $blueprints; - } - ], - 'toArray' => function () { - return [ - 'data' => $this->data, - 'errors' => $this->errors, - 'options' => [ - 'add' => $this->add, - 'empty' => $this->empty, - 'headline' => $this->headline, - 'help' => $this->help, - 'layout' => $this->layout, - 'link' => $this->link, - 'max' => $this->max, - 'min' => $this->min, - 'size' => $this->size, - 'sortable' => $this->sortable - ], - 'pagination' => $this->pagination, - ]; - } + return $blueprints; + } + ], + 'toArray' => function () { + return [ + 'data' => $this->data, + 'errors' => $this->errors, + 'options' => [ + 'add' => $this->add, + 'columns' => $this->columns, + 'empty' => $this->empty, + 'headline' => $this->headline, + 'help' => $this->help, + 'layout' => $this->layout, + 'link' => $this->link(), + 'max' => $this->max, + 'min' => $this->min, + 'search' => $this->search, + 'size' => $this->size, + 'sortable' => $this->sortable + ], + 'pagination' => $this->pagination, + ]; + } ]; diff --git a/kirby/config/sections/stats.php b/kirby/config/sections/stats.php new file mode 100644 index 0000000..e04755b --- /dev/null +++ b/kirby/config/sections/stats.php @@ -0,0 +1,62 @@ + [ + 'headline', + ], + 'props' => [ + /** + * Array or query string for reports. Each report needs a `label` and `value` and can have additional `info`, `link` 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; + }, + /** + * The size of the report cards. Available sizes: `tiny`, `small`, `medium`, `large` + */ + 'size' => function (string $size = 'large') { + return $size; + } + ], + 'computed' => [ + 'reports' => function () { + $reports = []; + $model = $this->model(); + $value = 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; + } + + $reports[] = [ + 'label' => I18n::translate($report['label'], $report['label']), + 'value' => $value($report['value'] ?? null), + 'info' => $value($report['info'] ?? null), + 'link' => $value($report['link'] ?? null), + 'theme' => $value($report['theme'] ?? null) + ]; + } + + return $reports; + } + ] +]; diff --git a/kirby/config/setup.php b/kirby/config/setup.php index eadb131..853b54b 100644 --- a/kirby/config/setup.php +++ b/kirby/config/setup.php @@ -11,11 +11,11 @@ define('DS', '/'); $aliases = require_once __DIR__ . '/aliases.php'; spl_autoload_register(function ($class) use ($aliases) { - $class = strtolower($class); + $class = strtolower($class); - if (isset($aliases[$class]) === true) { - class_alias($aliases[$class], $class); - } + if (isset($aliases[$class]) === true) { + class_alias($aliases[$class], $class); + } }); /** @@ -24,13 +24,13 @@ spl_autoload_register(function ($class) use ($aliases) { $testDir = dirname(__DIR__) . '/tests'; if (is_dir($testDir) === true) { - spl_autoload_register(function ($className) use ($testDir) { - $path = str_replace('Kirby\\', '', $className); - $path = str_replace('\\', '/', $path); - $file = $testDir . '/' . $path . '.php'; + spl_autoload_register(function ($className) use ($testDir) { + $path = str_replace('Kirby\\', '', $className); + $path = str_replace('\\', '/', $path); + $file = $testDir . '/' . $path . '.php'; - if (file_exists($file)) { - include $file; - } - }); + if (file_exists($file)) { + include $file; + } + }); } diff --git a/kirby/config/tags.php b/kirby/config/tags.php index efbd208..52232f8 100644 --- a/kirby/config/tags.php +++ b/kirby/config/tags.php @@ -9,314 +9,317 @@ use Kirby\Toolkit\Str; */ return [ - /** - * Date - */ - 'date' => [ - 'attr' => [], - 'html' => function ($tag) { - return strtolower($tag->date) === 'year' ? date('Y') : date($tag->date); - } - ], + /** + * Date + */ + 'date' => [ + 'attr' => [], + 'html' => function ($tag) { + return strtolower($tag->date) === 'year' ? date('Y') : date($tag->date); + } + ], - /** - * Email - */ - 'email' => [ - 'attr' => [ - 'class', - 'rel', - 'target', - 'text', - 'title' - ], - 'html' => function ($tag) { - return Html::email($tag->value, $tag->text, [ - 'class' => $tag->class, - 'rel' => $tag->rel, - 'target' => $tag->target, - 'title' => $tag->title, - ]); - } - ], + /** + * Email + */ + 'email' => [ + 'attr' => [ + 'class', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + return Html::email($tag->value, $tag->text, [ + 'class' => $tag->class, + 'rel' => $tag->rel, + 'target' => $tag->target, + 'title' => $tag->title, + ]); + } + ], - /** - * File - */ - 'file' => [ - 'attr' => [ - 'class', - 'download', - 'rel', - 'target', - 'text', - 'title' - ], - 'html' => function ($tag) { - if (!$file = $tag->file($tag->value)) { - return $tag->text; - } + /** + * File + */ + 'file' => [ + 'attr' => [ + 'class', + 'download', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + if (!$file = $tag->file($tag->value)) { + return $tag->text; + } - // use filename if the text is empty and make sure to - // ignore markdown italic underscores in filenames - if (empty($tag->text) === true) { - $tag->text = str_replace('_', '\_', $file->filename()); - } + // use filename if the text is empty and make sure to + // ignore markdown italic underscores in filenames + if (empty($tag->text) === true) { + $tag->text = str_replace('_', '\_', $file->filename()); + } - return Html::a($file->url(), $tag->text, [ - 'class' => $tag->class, - 'download' => $tag->download !== 'false', - 'rel' => $tag->rel, - 'target' => $tag->target, - 'title' => $tag->title, - ]); - } - ], + return Html::a($file->url(), $tag->text, [ + 'class' => $tag->class, + 'download' => $tag->download !== 'false', + 'rel' => $tag->rel, + 'target' => $tag->target, + 'title' => $tag->title, + ]); + } + ], - /** - * Gist - */ - 'gist' => [ - 'attr' => [ - 'file' - ], - 'html' => function ($tag) { - return Html::gist($tag->value, $tag->file); - } - ], + /** + * Gist + */ + 'gist' => [ + 'attr' => [ + 'file' + ], + 'html' => function ($tag) { + return Html::gist($tag->value, $tag->file); + } + ], - /** - * Image - */ - 'image' => [ - 'attr' => [ - 'alt', - 'caption', - 'class', - 'height', - 'imgclass', - 'link', - 'linkclass', - 'rel', - 'target', - 'title', - 'width' - ], - 'html' => function ($tag) { - if ($tag->file = $tag->file($tag->value)) { - $tag->src = $tag->file->url(); - $tag->alt = $tag->alt ?? $tag->file->alt()->or(' ')->value(); - $tag->title = $tag->title ?? $tag->file->title()->value(); - $tag->caption = $tag->caption ?? $tag->file->caption()->value(); - } else { - $tag->src = Url::to($tag->value); - } + /** + * Image + */ + 'image' => [ + 'attr' => [ + 'alt', + 'caption', + 'class', + 'height', + 'imgclass', + 'link', + 'linkclass', + 'rel', + 'target', + 'title', + 'width' + ], + 'html' => function ($tag) { + if ($tag->file = $tag->file($tag->value)) { + $tag->src = $tag->file->url(); + $tag->alt = $tag->alt ?? $tag->file->alt()->or(' ')->value(); + $tag->title = $tag->title ?? $tag->file->title()->value(); + $tag->caption = $tag->caption ?? $tag->file->caption()->value(); + } else { + $tag->src = Url::to($tag->value); + } - $link = function ($img) use ($tag) { - if (empty($tag->link) === true) { - return $img; - } + $link = function ($img) use ($tag) { + if (empty($tag->link) === true) { + return $img; + } - if ($link = $tag->file($tag->link)) { - $link = $link->url(); - } else { - $link = $tag->link === 'self' ? $tag->src : $tag->link; - } + if ($link = $tag->file($tag->link)) { + $link = $link->url(); + } else { + $link = $tag->link === 'self' ? $tag->src : $tag->link; + } - return Html::a($link, [$img], [ - 'rel' => $tag->rel, - 'class' => $tag->linkclass, - 'target' => $tag->target - ]); - }; + return Html::a($link, [$img], [ + 'rel' => $tag->rel, + 'class' => $tag->linkclass, + 'target' => $tag->target + ]); + }; - $image = Html::img($tag->src, [ - 'width' => $tag->width, - 'height' => $tag->height, - 'class' => $tag->imgclass, - 'title' => $tag->title, - 'alt' => $tag->alt ?? ' ' - ]); + $image = Html::img($tag->src, [ + 'width' => $tag->width, + 'height' => $tag->height, + 'class' => $tag->imgclass, + 'title' => $tag->title, + 'alt' => $tag->alt ?? ' ' + ]); - if ($tag->kirby()->option('kirbytext.image.figure', true) === false) { - return $link($image); - } + if ($tag->kirby()->option('kirbytext.image.figure', true) === false) { + return $link($image); + } - // render KirbyText in caption - if ($tag->caption) { - $tag->caption = [$tag->kirby()->kirbytext($tag->caption, [], true)]; - } + // render KirbyText in caption + if ($tag->caption) { + $options = ['markdown' => ['inline' => true]]; + $caption = $tag->kirby()->kirbytext($tag->caption, $options); + $tag->caption = [$caption]; + } - return Html::figure([ $link($image) ], $tag->caption, [ - 'class' => $tag->class - ]); - } - ], + return Html::figure([ $link($image) ], $tag->caption, [ + 'class' => $tag->class + ]); + } + ], - /** - * Link - */ - 'link' => [ - 'attr' => [ - 'class', - 'lang', - 'rel', - 'role', - 'target', - 'title', - 'text', - ], - 'html' => function ($tag) { - if (empty($tag->lang) === false) { - $tag->value = Url::to($tag->value, $tag->lang); - } + /** + * Link + */ + 'link' => [ + 'attr' => [ + 'class', + 'lang', + 'rel', + 'role', + 'target', + 'title', + 'text', + ], + 'html' => function ($tag) { + if (empty($tag->lang) === false) { + $tag->value = Url::to($tag->value, $tag->lang); + } - return Html::a($tag->value, $tag->text, [ - 'rel' => $tag->rel, - 'class' => $tag->class, - 'role' => $tag->role, - 'title' => $tag->title, - 'target' => $tag->target, - ]); - } - ], + return Html::a($tag->value, $tag->text, [ + 'rel' => $tag->rel, + 'class' => $tag->class, + 'role' => $tag->role, + 'title' => $tag->title, + 'target' => $tag->target, + ]); + } + ], - /** - * Tel - */ - 'tel' => [ - 'attr' => [ - 'class', - 'rel', - 'text', - 'title' - ], - 'html' => function ($tag) { - return Html::tel($tag->value, $tag->text, [ - 'class' => $tag->class, - 'rel' => $tag->rel, - 'title' => $tag->title - ]); - } - ], + /** + * Tel + */ + 'tel' => [ + 'attr' => [ + 'class', + 'rel', + 'text', + 'title' + ], + 'html' => function ($tag) { + return Html::tel($tag->value, $tag->text, [ + 'class' => $tag->class, + 'rel' => $tag->rel, + 'title' => $tag->title + ]); + } + ], - /** - * Twitter - */ - 'twitter' => [ - 'attr' => [ - 'class', - 'rel', - 'target', - 'text', - 'title' - ], - 'html' => function ($tag) { + /** + * Twitter + */ + 'twitter' => [ + 'attr' => [ + 'class', + 'rel', + 'target', + 'text', + 'title' + ], + 'html' => function ($tag) { + // get and sanitize the username + $username = str_replace('@', '', $tag->value); - // get and sanitize the username - $username = str_replace('@', '', $tag->value); + // build the profile url + $url = 'https://twitter.com/' . $username; - // build the profile url - $url = 'https://twitter.com/' . $username; + // sanitize the link text + $text = $tag->text ?? '@' . $username; - // sanitize the link text - $text = $tag->text ?? '@' . $username; + // build the final link + return Html::a($url, $text, [ + 'class' => $tag->class, + 'rel' => $tag->rel, + 'target' => $tag->target, + 'title' => $tag->title, + ]); + } + ], - // build the final link - return Html::a($url, $text, [ - 'class' => $tag->class, - 'rel' => $tag->rel, - 'target' => $tag->target, - 'title' => $tag->title, - ]); - } - ], + /** + * Video + */ + 'video' => [ + 'attr' => [ + 'autoplay', + 'caption', + 'controls', + 'class', + 'height', + 'loop', + 'muted', + 'playsinline', + 'poster', + 'preload', + 'style', + 'width', + ], + 'html' => function ($tag) { + // checks and gets if poster is local file + if ( + empty($tag->poster) === false && + Str::startsWith($tag->poster, 'http://') !== true && + Str::startsWith($tag->poster, 'https://') !== true + ) { + if ($poster = $tag->file($tag->poster)) { + $tag->poster = $poster->url(); + } + } - /** - * Video - */ - 'video' => [ - 'attr' => [ - 'autoplay', - 'caption', - 'controls', - 'class', - 'height', - 'loop', - 'muted', - 'poster', - 'preload', - 'style', - 'width', - ], - 'html' => function ($tag) { - // checks and gets if poster is local file - if ( - empty($tag->poster) === false && - Str::startsWith($tag->poster, 'http://') !== true && - Str::startsWith($tag->poster, 'https://') !== true - ) { - if ($poster = $tag->file($tag->poster)) { - $tag->poster = $poster->url(); - } - } + // checks video is local or provider(remote) + $isLocalVideo = ( + Str::startsWith($tag->value, 'http://') !== true && + Str::startsWith($tag->value, 'https://') !== true + ); + $isProviderVideo = ( + $isLocalVideo === false && + ( + Str::contains($tag->value, 'youtu', true) === true || + Str::contains($tag->value, 'vimeo', true) === true + ) + ); - // checks video is local or provider(remote) - $isLocalVideo = ( - Str::startsWith($tag->value, 'http://') !== true && - Str::startsWith($tag->value, 'https://') !== true - ); - $isProviderVideo = ( - $isLocalVideo === false && - ( - Str::contains($tag->value, 'youtu', true) === true || - Str::contains($tag->value, 'vimeo', true) === true - ) - ); + // default attributes for local and remote videos + $attrs = [ + 'height' => $tag->height, + 'width' => $tag->width + ]; - // default attributes for local and remote videos - $attrs = [ - 'height' => $tag->height, - 'width' => $tag->width - ]; + // don't use attributes that iframe doesn't support + if ($isProviderVideo === false) { + // converts tag attributes to supported formats (listed below) to output correct html + // booleans: autoplay, controls, loop, muted + // strings : poster, preload + // for ex : `autoplay` will not work if `false` is a `string` instead of a `boolean` + $attrs['autoplay'] = $autoplay = Str::toType($tag->autoplay, 'bool'); + $attrs['controls'] = Str::toType($tag->controls ?? true, 'bool'); + $attrs['loop'] = Str::toType($tag->loop, 'bool'); + $attrs['muted'] = Str::toType($tag->muted ?? $autoplay, 'bool'); + $attrs['playsinline'] = Str::toType($tag->playsinline ?? $autoplay, 'bool'); + $attrs['poster'] = $tag->poster; + $attrs['preload'] = $tag->preload; + } - // don't use attributes that iframe doesn't support - if ($isProviderVideo === false) { - // converts tag attributes to supported formats (listed below) to output correct html - // booleans: autoplay, controls, loop, muted - // strings : poster, preload - // for ex : `autoplay` will not work if `false` is a `string` instead of a `boolean` - $attrs['autoplay'] = $autoplay = Str::toType($tag->autoplay, 'bool'); - $attrs['controls'] = Str::toType($tag->controls ?? true, 'bool'); - $attrs['loop'] = Str::toType($tag->loop, 'bool'); - $attrs['muted'] = Str::toType($tag->muted ?? $autoplay, 'bool'); - $attrs['poster'] = $tag->poster; - $attrs['preload'] = $tag->preload; - } + // handles local and remote video file + if ($isLocalVideo === true) { + // handles local video file + if ($tag->file = $tag->file($tag->value)) { + $source = Html::tag('source', '', [ + 'src' => $tag->file->url(), + 'type' => $tag->file->mime() + ]); + $video = Html::tag('video', [$source], $attrs); + } + } else { + $video = Html::video( + $tag->value, + $tag->kirby()->option('kirbytext.video.options', []), + $attrs + ); + } - // handles local and remote video file - if ($isLocalVideo === true) { - // handles local video file - if ($tag->file = $tag->file($tag->value)) { - $source = Html::tag('source', '', [ - 'src' => $tag->file->url(), - 'type' => $tag->file->mime() - ]); - $video = Html::tag('video', [$source], $attrs); - } - } else { - $video = Html::video( - $tag->value, - $tag->kirby()->option('kirbytext.video.options', []), - $attrs - ); - } - - return Html::figure([$video ?? ''], $tag->caption, [ - 'class' => $tag->class ?? 'video', - 'style' => $tag->style - ]); - } - ], + return Html::figure([$video ?? ''], $tag->caption, [ + 'class' => $tag->class ?? 'video', + 'style' => $tag->style + ]); + } + ], ]; diff --git a/kirby/config/templates/emails/auth/login.php b/kirby/config/templates/emails/auth/login.php index e552481..cacea18 100644 --- a/kirby/config/templates/emails/auth/login.php +++ b/kirby/config/templates/emails/auth/login.php @@ -9,8 +9,8 @@ use Kirby\Toolkit\I18n; * @var int $timeout */ echo I18n::template( - 'login.email.login.body', - null, - compact('user', 'site', 'code', 'timeout'), - $user->language() + 'login.email.login.body', + null, + compact('user', 'site', 'code', 'timeout'), + $user->language() ); diff --git a/kirby/config/templates/emails/auth/password-reset.php b/kirby/config/templates/emails/auth/password-reset.php index 3cd55de..4480f31 100644 --- a/kirby/config/templates/emails/auth/password-reset.php +++ b/kirby/config/templates/emails/auth/password-reset.php @@ -9,8 +9,8 @@ use Kirby\Toolkit\I18n; * @var int $timeout */ echo I18n::template( - 'login.email.password-reset.body', - null, - compact('user', 'site', 'code', 'timeout'), - $user->language() + 'login.email.password-reset.body', + null, + compact('user', 'site', 'code', 'timeout'), + $user->language() ); diff --git a/kirby/i18n/translations/bg.json b/kirby/i18n/translations/bg.json index e44802d..64e1823 100644 --- a/kirby/i18n/translations/bg.json +++ b/kirby/i18n/translations/bg.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Invalid code", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Hour", "import": "Import", + "info": "Info", "insert": "\u0412\u043c\u044a\u043a\u043d\u0438", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "\u041b\u0438\u0446\u0435\u043d\u0437 \u0437\u0430 Kirby", "license.buy": "Купи лиценз", "license.register": "Регистрирай", + "license.manage": "Manage your licenses", "license.register.help": "You received your license code after the purchase via email. Please copy and paste it to register.", "license.register.label": "Please enter your license code", "license.register.success": "Thank you for supporting Kirby", "license.unregistered": "Това е нерегистрирана демо версия на Kirby", + "license.unregistered.label": "Unregistered", "link": "\u0412\u0440\u044a\u0437\u043a\u0430", "link.text": "Текстова връзка", @@ -354,7 +360,7 @@ "lock.unlock": "Unlock", "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", - "login": "Вход", + "login": "Подписване", "login.code.label.login": "Login code", "login.code.label.password-reset": "Password reset code", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "The section is required", + "security": "Security", "select": "Избери", + "server": "Server", "settings": "Настройки", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Размер", "slug": "URL-\u0434\u043e\u0431\u0430\u0432\u043a\u0430", "sort": "Сортирай", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Заглавие", "template": "Образец", "today": "Днес", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Код", "toolbar.button.bold": "\u041f\u043e\u043b\u0443\u0447\u0435\u0440 \u0448\u0440\u0438\u0444\u0442", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/ca.json b/kirby/i18n/translations/ca.json index 1fd47c0..026491e 100644 --- a/kirby/i18n/translations/ca.json +++ b/kirby/i18n/translations/ca.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@exemple.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Codi invàlid", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Hora", "import": "Import", + "info": "Info", "insert": "Insertar", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "Llic\u00e8ncia Kirby", "license.buy": "Comprar una llicència", "license.register": "Registrar", + "license.manage": "Manage your licenses", "license.register.help": "Heu rebut el codi de la vostra llicència després de la compra, per correu electrònic. Copieu-lo i enganxeu-lo per registrar-vos.", "license.register.label": "Si us plau, introdueixi el seu codi de llicència", "license.register.success": "Gràcies per donar suport a Kirby", "license.unregistered": "Aquesta és una demo no registrada de Kirby", + "license.unregistered.label": "Unregistered", "link": "Enlla\u00e7", "link.text": "Enllaç de text", @@ -469,20 +475,28 @@ "section.required": "La secció és obligatòria", + "security": "Security", "select": "Seleccionar", + "server": "Server", "settings": "Configuració", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Tamany", "slug": "URL-ap\u00e8ndix", "sort": "Ordenar", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Títol", "template": "Plantilla", "today": "Avui", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Codi", "toolbar.button.bold": "Negreta", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/cs.json b/kirby/i18n/translations/cs.json index a5670b6..ba19b77 100644 --- a/kirby/i18n/translations/cs.json +++ b/kirby/i18n/translations/cs.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Prostředí", "error.access.code": "Neplatný kód", @@ -294,6 +297,7 @@ "hide": "Skrýt", "hour": "Hodina", "import": "Import", + "info": "Info", "insert": "Vlo\u017eit", "insert.after": "Vložit za", "insert.before": "Vložit před", @@ -336,10 +340,12 @@ "license": "Kirby licence", "license.buy": "Zakoupit licenci", "license.register": "Registrovat", + "license.manage": "Manage your licenses", "license.register.help": "Licenční kód jste po zakoupení obdrželi na email. Vložte prosím kód a zaregistrujte Vaší kopii.", "license.register.label": "Zadejte prosím licenční kód", "license.register.success": "Děkujeme Vám za podporu Kirby", "license.unregistered": "Toto je neregistrovaná kopie Kirby", + "license.unregistered.label": "Unregistered", "link": "Odkaz", "link.text": "Text odkazu", @@ -469,20 +475,28 @@ "section.required": "Sekce musí být vyplněna", + "security": "Security", "select": "Vybrat", + "server": "Server", "settings": "Nastavení", "show": "Zobrazit", + "site.blueprint": "Hlavní panel nemá blueprint. Blueprint můžete definovat v /site/blueprints/site.yml", "size": "Velikost", "slug": "P\u0159\u00edpona URL", "sort": "Řadit", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Název", "template": "\u0160ablona", "today": "Dnes", - "server": "Server", - - "site.blueprint": "Hlavní panel nemá blueprint. Blueprint můžete definovat v /site/blueprints/site.yml", - "toolbar.button.code": "Kód", "toolbar.button.bold": "Tu\u010dn\u00fd text", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/da.json b/kirby/i18n/translations/da.json index 0fe9b9d..e71e9c5 100644 --- a/kirby/i18n/translations/da.json +++ b/kirby/i18n/translations/da.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@eksempel.dk", + "entries": "Entries", + "entry": "Entry", + "environment": "Miljø", "error.access.code": "Ugyldig kode", @@ -294,6 +297,7 @@ "hide": "Skjul", "hour": "Time", "import": "Importer", + "info": "Info", "insert": "Inds\u00e6t", "insert.after": "Indsæt efter", "insert.before": "Indsæt før", @@ -336,10 +340,12 @@ "license": "Kirby licens", "license.buy": "Køb en licens", "license.register": "Registrer", + "license.manage": "Manage your licenses", "license.register.help": "Du modtog din licenskode efter købet via email. Venligst kopier og indsæt den for at registrere.", "license.register.label": "Indtast venligst din licenskode", "license.register.success": "Tak for din støtte af Kirby", "license.unregistered": "Dette er en uregistreret demo af Kirby", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Link tekst", @@ -396,7 +402,7 @@ "next": "Næste", "no": "nej", "off": "Sluk", - "on": "Tænd", + "on": "Aktiveret", "open": "Åben", "open.newWindow": "Åben i et nyt vindue", "options": "Indstillinger", @@ -469,20 +475,28 @@ "section.required": "Sektionen er påkrævet", + "security": "Security", "select": "Vælg", + "server": "Server", "settings": "Indstillinger", "show": "Vis", + "site.blueprint": "Sitet har intet blueprint endnu. Du kan definere opsætningen i /site/blueprints/site.yml", "size": "Størrelse", "slug": "URL-appendiks", "sort": "Sorter", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titel", "template": "Skabelon", "today": "Idag", - "server": "Server", - - "site.blueprint": "Sitet har intet blueprint endnu. Du kan definere opsætningen i /site/blueprints/site.yml", - "toolbar.button.code": "Kode", "toolbar.button.bold": "Fed tekst", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/de.json b/kirby/i18n/translations/de.json index ebee7ca..ae1052e 100644 --- a/kirby/i18n/translations/de.json +++ b/kirby/i18n/translations/de.json @@ -49,6 +49,9 @@ "email": "E-Mail", "email.placeholder": "mail@beispiel.de", + "entries": "Einträge", + "entry": "Eintrag", + "environment": "Umgebung", "error.access.code": "Ungültiger Code", @@ -294,6 +297,7 @@ "hide": "Verbergen", "hour": "Stunde", "import": "Importieren", + "info": "Info", "insert": "Einf\u00fcgen", "insert.after": "Danach einfügen", "insert.before": "Davor einfügen", @@ -336,10 +340,12 @@ "license": "Lizenz", "license.buy": "Kaufe eine Lizenz", "license.register": "Registrieren", + "license.manage": "Verwalte deine Lizenzen", "license.register.help": "Den Lizenzcode findest du in der Bestätigungsmail zu deinem Kauf. Bitte kopiere und füge ihn ein, um Kirby zu registrieren.", "license.register.label": "Bitte gib deinen Lizenzcode ein", "license.register.success": "Vielen Dank für deine Unterstützung", "license.unregistered": "Dies ist eine unregistrierte Kirby-Demo", + "license.unregistered.label": "Unregistriert", "link": "Link", "link.text": "Linktext", @@ -469,20 +475,28 @@ "section.required": "Der Bereich ist Pflicht", + "security": "Sicherheit", "select": "Auswählen", + "server": "Server", "settings": "Einstellungen", "show": "Anzeigen", + "site.blueprint": "Du kannst zusätzliche Felder und Bereiche für die Seite in /site/blueprints/site.yml anlegen", "size": "Größe", "slug": "URL-Anhang", "sort": "Sortieren", + + "stats.empty": "Keine Daten", + "system.issues.content": "Der content Ordner scheint öffentlich zugänglich zu sein", + "system.issues.debug": "Debugging muss im öffentlichen Betrieb ausgeschaltet sein", + "system.issues.git": "Der .git Ordner scheint öffentlich zugänglich zu sein", + "system.issues.https": "Wir empfehlen HTTPS für alle deine Seiten", + "system.issues.kirby": "Der kirby Ordner scheint öffentlich zugänglich zu sein", + "system.issues.site": "Der site Ordner scheint öffentlich zugänglich zu sein", + "title": "Titel", "template": "Vorlage", "today": "Heute", - "server": "Server", - - "site.blueprint": "Du kannst zusätzliche Felder und Bereiche für die Seite in /site/blueprints/site.yml anlegen", - "toolbar.button.code": "Code", "toolbar.button.bold": "Fetter Text", "toolbar.button.email": "E-Mail", diff --git a/kirby/i18n/translations/el.json b/kirby/i18n/translations/el.json index b0dc5d0..e61f2c5 100644 --- a/kirby/i18n/translations/el.json +++ b/kirby/i18n/translations/el.json @@ -49,6 +49,9 @@ "email": "Διεύθυνση ηλεκτρονικού ταχυδρομείου", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Mη έγκυρος κωδικός", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Ώρα", "import": "Import", + "info": "Info", "insert": "\u0395\u03b9\u03c3\u03b1\u03b3\u03c9\u03b3\u03ae", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "\u0386\u03b4\u03b5\u03b9\u03b1 \u03a7\u03c1\u03ae\u03c3\u03b7\u03c2 \u03c4\u03bf\u03c5 Kirby", "license.buy": "Αγοράστε μια άδεια", "license.register": "Εγγραφή", + "license.manage": "Manage your licenses", "license.register.help": "Έχετε λάβει τον κωδικό άδειας χρήσης μετά την αγορά μέσω ηλεκτρονικού ταχυδρομείου. Παρακαλώ αντιγράψτε και επικολλήστε τον για να εγγραφείτε.", "license.register.label": "Παρακαλώ εισαγάγετε τον κωδικό άδειας χρήσης", "license.register.success": "Σας ευχαριστούμε για την υποστήριξη του Kirby", "license.unregistered": "Αυτό είναι ένα μη καταχωρημένο demo του Kirby", + "license.unregistered.label": "Unregistered", "link": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03bc\u03bf\u03c2", "link.text": "\u039a\u03b5\u03af\u03bc\u03b5\u03bd\u03bf \u03c3\u03c5\u03bd\u03b4\u03ad\u03c3\u03bc\u03bf\u03c5", @@ -354,7 +360,7 @@ "lock.unlock": "Unlock", "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", - "login": "\u03a3\u03cd\u03bd\u03b4\u03b5\u03c3\u03b7", + "login": "Σύνδεση", "login.code.label.login": "Login code", "login.code.label.password-reset": "Password reset code", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "The section is required", + "security": "Security", "select": "Επιλογή", + "server": "Server", "settings": "Ρυθμίσεις", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Μέγεθος", "slug": "\u0395\u03c0\u03af\u03b8\u03b5\u03bc\u03b1 URL", "sort": "Ταξινόμηση", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Τίτλος", "template": "\u03a0\u03c1\u03cc\u03c4\u03c5\u03c0\u03bf", "today": "Σήμερα", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Κώδικας", "toolbar.button.bold": "\u0388\u03bd\u03c4\u03bf\u03bd\u03b7 \u03b3\u03c1\u03b1\u03c6\u03ae", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/en.json b/kirby/i18n/translations/en.json index 7b328f4..8c98cab 100644 --- a/kirby/i18n/translations/en.json +++ b/kirby/i18n/translations/en.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Invalid code", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Hour", "import": "Import", + "info": "Info", "insert": "Insert", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "License", "license.buy": "Buy a license", "license.register": "Register", + "license.manage": "Manage your licenses", "license.register.help": "You received your license code after the purchase via email. Please copy and paste it to register.", "license.register.label": "Please enter your license code", "license.register.success": "Thank you for supporting Kirby", "license.unregistered": "This is an unregistered demo of Kirby", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Link text", @@ -354,7 +360,7 @@ "lock.unlock": "Unlock", "lock.isUnlocked": "Your unsaved changes have been overwritten by another user. You can download your changes to merge them manually.", - "login": "Login", + "login": "Log in", "login.code.label.login": "Login code", "login.code.label.password-reset": "Password reset code", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "The section is required", + "security": "Security", "select": "Select", + "server": "Server", "settings": "Settings", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Size", "slug": "URL appendix", "sort": "Sort", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Title", "template": "Template", "today": "Today", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Code", "toolbar.button.bold": "Bold", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/eo.json b/kirby/i18n/translations/eo.json index 835d15e..f7bb72f 100644 --- a/kirby/i18n/translations/eo.json +++ b/kirby/i18n/translations/eo.json @@ -49,6 +49,9 @@ "email": "Retpoŝto", "email.placeholder": "retpoŝto@ekzemplo.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Medio", "error.access.code": "Nevalida kodo", @@ -294,6 +297,7 @@ "hide": "Kaŝi", "hour": "Horo", "import": "Importi", + "info": "Info", "insert": "Enmeti", "insert.after": "Enmeti post", "insert.before": "Enmeti antaŭ", @@ -336,10 +340,12 @@ "license": "Permisilo", "license.buy": "Aĉeti permisilon", "license.register": "Registriĝi", + "license.manage": "Manage your licenses", "license.register.help": "Vi ricevis vian kodon de permisilo retpoŝte, post aĉeti ĝin. Bonvolu kopii kaj alglui ĝin por registriĝi.", "license.register.label": "Bonvolu entajpi vian kodon de permisilo", "license.register.success": "Dankon pro subteni Kirby", "license.unregistered": "Ĉi tiu estas neregistrita kopio de Kirby", + "license.unregistered.label": "Unregistered", "link": "Ligilo", "link.text": "Ligila teksto", @@ -469,20 +475,28 @@ "section.required": "La sekcio estas deviga", + "security": "Security", "select": "Elekti", + "server": "Servilo", "settings": "Agordoj", "show": "Montri", + "site.blueprint": "La retejo ankoraŭ ne havas planon. Vi povas difini planon ĉe /site/blueprints/site.yml", "size": "Grando", "slug": "URL-nomo", "sort": "Ordigi", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titolo", "template": "Ŝablono", "today": "Hodiaŭ", - "server": "Servilo", - - "site.blueprint": "La retejo ankoraŭ ne havas planon. Vi povas difini planon ĉe /site/blueprints/site.yml", - "toolbar.button.code": "Kodo", "toolbar.button.bold": "Grasa", "toolbar.button.email": "Retpoŝto", diff --git a/kirby/i18n/translations/es_419.json b/kirby/i18n/translations/es_419.json index cc85712..57d8b4e 100644 --- a/kirby/i18n/translations/es_419.json +++ b/kirby/i18n/translations/es_419.json @@ -49,6 +49,9 @@ "email": "Correo Electrónico", "email.placeholder": "correo@ejemplo.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Ambiente", "error.access.code": "Código inválido", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Hora", "import": "Import", + "info": "Info", "insert": "Insertar", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "Licencia", "license.buy": "Comprar una licencia", "license.register": "Registrar", + "license.manage": "Manage your licenses", "license.register.help": "Recibió su código de licencia después de la compra por correo electrónico. Por favor copie y pegue para registrarse.", "license.register.label": "Por favor, ingresa tu código de licencia", "license.register.success": "Gracias por apoyar a Kirby", "license.unregistered": "Este es un demo no registrado de Kirby", + "license.unregistered.label": "Unregistered", "link": "Enlace", "link.text": "Texto de Enlace", @@ -354,7 +360,7 @@ "lock.unlock": "Desbloquear", "lock.isUnlocked": "Tus cambios sin guardar han sido sobrescritos por otro usuario. Puedes descargar los cambios y fusionarlos manualmente.", - "login": "Iniciar sesi\u00f3n", + "login": "Iniciar sesión", "login.code.label.login": "Login code", "login.code.label.password-reset": "Password reset code", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "Esta sección es requerida", + "security": "Security", "select": "Seleccionar", + "server": "Server", "settings": "Ajustes", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Tamaño", "slug": "Apéndice URL", "sort": "Ordenar", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Título", "template": "Plantilla", "today": "Hoy", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Código", "toolbar.button.bold": "Negrita", "toolbar.button.email": "Email", @@ -490,9 +504,9 @@ "toolbar.button.heading.1": "Encabezado 1", "toolbar.button.heading.2": "Encabezado 2", "toolbar.button.heading.3": "Encabezado 3", - "toolbar.button.heading.4": "Heading 4", - "toolbar.button.heading.5": "Heading 5", - "toolbar.button.heading.6": "Heading 6", + "toolbar.button.heading.4": "Encabezado 4", + "toolbar.button.heading.5": "Encabezado 5", + "toolbar.button.heading.6": "Encabezado 6", "toolbar.button.italic": "Texto en It\u00e1licas", "toolbar.button.file": "Archivo", "toolbar.button.file.select": "Selecciona un archivo", diff --git a/kirby/i18n/translations/es_ES.json b/kirby/i18n/translations/es_ES.json index 1fc7575..470346a 100644 --- a/kirby/i18n/translations/es_ES.json +++ b/kirby/i18n/translations/es_ES.json @@ -49,6 +49,9 @@ "email": "Correo electrónico", "email.placeholder": "correo@ejemplo.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Ambiente", "error.access.code": "Código inválido", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Hora", "import": "Import", + "info": "Info", "insert": "Insertar", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "Licencia", "license.buy": "Comprar una licencia", "license.register": "Registro", + "license.manage": "Manage your licenses", "license.register.help": "Recibió su código de licencia después de la compra por correo electrónico. Por favor copie y pegue para registrarse.", "license.register.label": "Por favor ingrese su código de licencia", "license.register.success": "Gracias por apoyar a Kirby", "license.unregistered": "Esta es una demo no registrada de Kirby", + "license.unregistered.label": "Unregistered", "link": "Enlace", "link.text": "Texto del enlace", @@ -469,20 +475,28 @@ "section.required": "Esta sección es requerida", + "security": "Security", "select": "Seleccionar", + "server": "Server", "settings": "Ajustes", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Tamaño", "slug": "Apéndice URL", "sort": "Ordenar", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titulo", "template": "Plantilla", "today": "Hoy", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Código", "toolbar.button.bold": "Negritas", "toolbar.button.email": "Correo electrónico", @@ -490,9 +504,9 @@ "toolbar.button.heading.1": "Encabezado 1", "toolbar.button.heading.2": "Encabezado 2", "toolbar.button.heading.3": "Encabezado 3", - "toolbar.button.heading.4": "Heading 4", - "toolbar.button.heading.5": "Heading 5", - "toolbar.button.heading.6": "Heading 6", + "toolbar.button.heading.4": "Encabezado 4", + "toolbar.button.heading.5": "Encabezado 5", + "toolbar.button.heading.6": "Encabezado 6", "toolbar.button.italic": "Italica", "toolbar.button.file": "Archivo", "toolbar.button.file.select": "Seleccione un archivo", diff --git a/kirby/i18n/translations/fa.json b/kirby/i18n/translations/fa.json index 8836a12..3f6e2fc 100644 --- a/kirby/i18n/translations/fa.json +++ b/kirby/i18n/translations/fa.json @@ -49,6 +49,9 @@ "email": "\u067e\u0633\u062a \u0627\u0644\u06a9\u062a\u0631\u0648\u0646\u06cc\u06a9", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Invalid code", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "ساعت", "import": "Import", + "info": "Info", "insert": "\u062f\u0631\u062c", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "\u0645\u062c\u0648\u0632", "license.buy": "خرید مجوز", "license.register": "ثبت", + "license.manage": "Manage your licenses", "license.register.help": "پس از خرید از طریق ایمیل، کد مجوز خود را دریافت کردید. لطفا برای ثبت‌نام آن را کپی و اینجا پیست کنید.", "license.register.label": "لطفا کد مجوز خود را وارد کنید", "license.register.success": "با تشکر از شما برای حمایت از کربی", "license.unregistered": "این یک نسخه آزمایشی ثبت نشده از کربی است", + "license.unregistered.label": "Unregistered", "link": "\u067e\u06cc\u0648\u0646\u062f", "link.text": "\u0645\u062a\u0646 \u067e\u06cc\u0648\u0646\u062f", @@ -469,20 +475,28 @@ "section.required": "The section is required", + "security": "Security", "select": "انتخاب", + "server": "Server", "settings": "تنظیمات", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "اندازه", "slug": "پسوند Url", "sort": "ترتیب", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "عنوان", "template": "\u0642\u0627\u0644\u0628 \u0635\u0641\u062d\u0647", "today": "امروز", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "کد", "toolbar.button.bold": "\u0645\u062a\u0646 \u0628\u0627 \u062d\u0631\u0648\u0641 \u062f\u0631\u0634\u062a", "toolbar.button.email": "\u067e\u0633\u062a \u0627\u0644\u06a9\u062a\u0631\u0648\u0646\u06cc\u06a9", diff --git a/kirby/i18n/translations/fi.json b/kirby/i18n/translations/fi.json index 2fe0b2e..49b70c1 100644 --- a/kirby/i18n/translations/fi.json +++ b/kirby/i18n/translations/fi.json @@ -49,6 +49,9 @@ "email": "S\u00e4hk\u00f6posti", "email.placeholder": "nimi@osoite.fi", + "entries": "Entries", + "entry": "Entry", + "environment": "Ympäristö", "error.access.code": "Väärä koodi", @@ -294,6 +297,7 @@ "hide": "Piilota", "hour": "Tunti", "import": "Tuo", + "info": "Info", "insert": "Lis\u00e4\u00e4", "insert.after": "Lisää eteen", "insert.before": "Lisää jälkeen", @@ -336,10 +340,12 @@ "license": "Lisenssi", "license.buy": "Osta lisenssi", "license.register": "Rekisteröi", + "license.manage": "Manage your licenses", "license.register.help": "Lisenssiavain on lähetetty oston jälkeen sähköpostiisi. Kopioi ja liitä avain tähän.", "license.register.label": "Anna lisenssiavain", "license.register.success": "Kiitos kun tuet Kirbyä", "license.unregistered": "Tämä on rekisteröimätön demo Kirbystä", + "license.unregistered.label": "Unregistered", "link": "Linkki", "link.text": "Linkin teksti", @@ -469,20 +475,28 @@ "section.required": "Osio on pakollinen", + "security": "Security", "select": "Valitse", + "server": "Palvelin", "settings": "Asetukset", "show": "Näytä", + "site.blueprint": "Tällä sivustolla ei ole vielä suunnitelmaa. Voit määrittää suunnitelman tiedostoon /site/blueprints/site.yml", "size": "Koko", "slug": "URL-tunniste", "sort": "Järjestele", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Nimi", "template": "Sivupohja", "today": "Tänään", - "server": "Palvelin", - - "site.blueprint": "Tällä sivustolla ei ole vielä suunnitelmaa. Voit määrittää suunnitelman tiedostoon /site/blueprints/site.yml", - "toolbar.button.code": "Koodi", "toolbar.button.bold": "Lihavointi", "toolbar.button.email": "S\u00e4hk\u00f6posti", diff --git a/kirby/i18n/translations/fr.json b/kirby/i18n/translations/fr.json index 47339b1..9ccec12 100644 --- a/kirby/i18n/translations/fr.json +++ b/kirby/i18n/translations/fr.json @@ -1,7 +1,7 @@ { "account.changeName": "Modifier votre nom", "account.delete": "Supprimer votre compte", - "account.delete.confirm": "Voulez-vous vraiment supprimer votre compte ? Vous serez déconnecté immédiatement. Votre compte ne pourra pas être récupéré.", + "account.delete.confirm": "Voulez-vous vraiment supprimer votre compte ? Vous serez déconnecté immédiatement. Votre compte ne pourra pas être récupéré.", "add": "Ajouter", "author": "Auteur", @@ -18,7 +18,7 @@ "create": "Créer", "date": "Date", - "date.select": "Choisissez une date", + "date.select": "Choisir une date", "day": "Jour", "days.fri": "Ven", @@ -49,6 +49,9 @@ "email": "Courriel", "email.placeholder": "mail@example.com", + "entries": "Entrées", + "entry": "Entrée", + "environment": "Environnement", "error.access.code": "Code incorrect", @@ -61,7 +64,7 @@ "error.avatar.dimensions.invalid": "Veuillez choisir une image de profil de largeur et hauteur inférieures à 3000 pixels", "error.avatar.mime.forbidden": "L'image du profil utilisateur doit être un fichier JPEG ou PNG", - "error.blueprint.notFound": "Le blueprint « {name} » n’a pu être chargé", + "error.blueprint.notFound": "Le blueprint « {name} » n’a pu être chargé", "error.blocks.max.plural": "Vous ne devez pas ajouter plus de {max} blocs", "error.blocks.max.singular": "Vous ne devez pas ajouter plus d'un bloc", @@ -69,29 +72,29 @@ "error.blocks.min.singular": "Vous devez ajouter au moins un bloc", "error.blocks.validation": "Il y a une erreur dans le bloc {index}", - "error.email.preset.notFound": "La configuration de courriel « {name} » n’a pu être trouvé", + "error.email.preset.notFound": "La configuration de courriel « {name} » n’a pu être trouvé ", - "error.field.converter.invalid": "Convertisseur « {converter} » incorrect", + "error.field.converter.invalid": "Convertisseur « {converter} » incorrect", "error.file.changeName.empty": "Le nom ne peut être vide", - "error.file.changeName.permission": "Vous n’êtes pas autorisé à modifier le nom de « {filename} »", - "error.file.duplicate": "Un fichier nommé « {filename} » existe déjà", - "error.file.extension.forbidden": "L’extension « {extension} » n’est pas autorisée", + "error.file.changeName.permission": "Vous n’êtes pas autorisé à modifier le nom de « {filename} »", + "error.file.duplicate": "Un fichier nommé « {filename} » existe déjà", + "error.file.extension.forbidden": "L’extension « {extension} » n’est pas autorisée", "error.file.extension.invalid": "Extension non valide : {extension}", - "error.file.extension.missing": "L’extension pour « {filename} » est manquante", + "error.file.extension.missing": "L’extension pour « {filename} » est manquante", "error.file.maxheight": "La hauteur de l'image ne doit pas excéder {height} pixels", "error.file.maxsize": "Le fichier est trop volumineux", "error.file.maxwidth": "La largeur de l'image ne doit pas excéder {width} pixels", - "error.file.mime.differs": "Le fichier transféré doit être du même type de média « {mime} »", - "error.file.mime.forbidden": "Le type de média « {mime} » n’est pas autorisé", + "error.file.mime.differs": "Le fichier transféré doit être du même type de média « {mime} »", + "error.file.mime.forbidden": "Le type de média « {mime} » n’est pas autorisé", "error.file.mime.invalid": "Type de média non valide : {mime}", - "error.file.mime.missing": "Le type de média de « {filename} » n’a pu être détecté", + "error.file.mime.missing": "Le type de média de « {filename} » n’a pu être détecté", "error.file.minheight": "La hauteur de l'image doit être au moins {height} pixels", "error.file.minsize": "Le fichier n'est pas assez volumineux", "error.file.minwidth": "La largeur de l'image doit être au moins {width} pixels", "error.file.name.missing": "Veuillez entrer un titre", - "error.file.notFound": "Le fichier « {filename} » n’a pu être trouvé", - "error.file.orientation": "L'orientation de l'image doit être \"{orientation}\"", + "error.file.notFound": "Le fichier « {filename} » n’a pu être trouvé", + "error.file.orientation": "L'orientation de l'image doit être « {orientation} »", "error.file.type.forbidden": "Vous n’êtes pas autorisé à transférer des fichiers {type}", "error.file.type.invalid": "Type de fichier non valide : {type}", "error.file.undefined": "Le fichier n’a pu être trouvé", @@ -113,43 +116,43 @@ "error.offline": "Le Panel est actuellement hors ligne", - "error.page.changeSlug.permission": "Vous n’êtes pas autorisé à modifier l’identifiant d’URL pour « {slug} »", + "error.page.changeSlug.permission": "Vous n’êtes pas autorisé à modifier l’identifiant d’URL pour « {slug} »", "error.page.changeStatus.incomplete": "La page comporte des erreurs et ne peut pas être publiée", "error.page.changeStatus.permission": "Le statut de cette page ne peut être modifié", - "error.page.changeStatus.toDraft.invalid": "La page « {slug} » ne peut être convertie en brouillon", - "error.page.changeTemplate.invalid": "Le modèle de la page « {slug} » ne peut être changé", - "error.page.changeTemplate.permission": "Vous n’êtes pas autorisé à changer le modèle de « {slug} »", + "error.page.changeStatus.toDraft.invalid": "La page « {slug} » ne peut être convertie en brouillon", + "error.page.changeTemplate.invalid": "Le modèle de la page « {slug} » ne peut être changé", + "error.page.changeTemplate.permission": "Vous n’êtes pas autorisé à changer le modèle de « {slug} »", "error.page.changeTitle.empty": "Le titre ne peut être vide", - "error.page.changeTitle.permission": "Vous n’êtes pas autorisé à modifier le titre de « {slug} »", - "error.page.create.permission": "Vous n’êtes pas autorisé à créer « {slug} »", - "error.page.delete": "La page « {slug} » ne peut être supprimée", + "error.page.changeTitle.permission": "Vous n’êtes pas autorisé à modifier le titre de « {slug} »", + "error.page.create.permission": "Vous n’êtes pas autorisé à créer « {slug} »", + "error.page.delete": "La page « {slug} » ne peut être supprimée", "error.page.delete.confirm": "Veuillez saisir le titre de la page pour confirmer", "error.page.delete.hasChildren": "La page comporte des sous-pages et ne peut pas être supprimée", - "error.page.delete.permission": "Vous n’êtes pas autorisé à supprimer « {slug} »", - "error.page.draft.duplicate": "Un brouillon avec l’identifiant d’URL « {slug} » existe déjà", - "error.page.duplicate": "Une page avec l’identifiant d’URL « {slug} » existe déjà", - "error.page.duplicate.permission": "Vous n'êtes pas autorisé à dupliquer « {slug} »", - "error.page.notFound": "La page « {slug} » n’a pu être trouvée", + "error.page.delete.permission": "Vous n’êtes pas autorisé à supprimer « {slug} »", + "error.page.draft.duplicate": "Un brouillon avec l’identifiant d’URL « {slug} » existe déjà", + "error.page.duplicate": "Une page avec l’identifiant d’URL « {slug} » existe déjà", + "error.page.duplicate.permission": "Vous n'êtes pas autorisé à dupliquer « {slug} »", + "error.page.notFound": "La page « {slug} » n’a pu être trouvée", "error.page.num.invalid": "Veuillez saisir un numéro de position valide. Les numéros ne doivent pas être négatifs.", "error.page.slug.invalid": "Veuillez entrer un identifiant d’URL valide", - "error.page.slug.maxlength": "L’identifiant d’URL doit faire moins de \"{length}\" caractères", - "error.page.sort.permission": "La page « {slug} » ne peut être réordonnée", + "error.page.slug.maxlength": "L’identifiant d’URL doit faire moins de « {length} » caractères", + "error.page.sort.permission": "La page « {slug} » ne peut être réordonnée", "error.page.status.invalid": "Veuillez choisir un statut de page valide", "error.page.undefined": "La page n’a pu être trouvée", - "error.page.update.permission": "Vous n’êtes pas autorisé à modifier « {slug} »", + "error.page.update.permission": "Vous n’êtes pas autorisé à modifier « {slug} »", - "error.section.files.max.plural": "Vous ne pouvez ajouter plus de {max} fichier(s) à la section « {section} »", - "error.section.files.max.singular": "Vous ne pouvez ajouter plus d’un fichier à la section « {section} »", - "error.section.files.min.plural": "La section « {section}\" » requiert au moins {min} fichiers", - "error.section.files.min.singular": "La section « {section}\" » requiert au moins un fichier", + "error.section.files.max.plural": "Vous ne pouvez ajouter plus de {max} fichier(s) à la section « {section} »", + "error.section.files.max.singular": "Vous ne pouvez ajouter plus d’un fichier à la section « {section} »", + "error.section.files.min.plural": "La section « {section} » requiert au moins {min} fichiers", + "error.section.files.min.singular": "La section « {section} » requiert au moins un fichier", - "error.section.pages.max.plural": "Vous ne pouvez ajouter plus de {max} pages à la section « {section} »", - "error.section.pages.max.singular": "Vous ne pouvez ajouter plus d’une page à la section « {section} »", - "error.section.pages.min.plural": "La section « {section}\" » requiert au moins {min} pages", - "error.section.pages.min.singular": "La section « {section}\" » requiert au moins une page", + "error.section.pages.max.plural": "Vous ne pouvez ajouter plus de {max} pages à la section « {section} »", + "error.section.pages.max.singular": "Vous ne pouvez ajouter plus d’une page à la section « {section} »", + "error.section.pages.min.plural": "La section « {section} » requiert au moins {min} pages", + "error.section.pages.min.singular": "La section « {section} » requiert au moins une page", - "error.section.notLoaded": "La section « {name} » n’a pu être chargée", - "error.section.type.invalid": "Le type de section « {type} » est incorrect", + "error.section.notLoaded": "La section « {name} » n’a pu être chargée", + "error.section.type.invalid": "Le type de section « {type} » est incorrect", "error.site.changeTitle.empty": "Le titre ne peut être vide", "error.site.changeTitle.permission": "Vous n’êtes pas autorisé à modifier le titre du site", @@ -157,46 +160,46 @@ "error.template.default.notFound": "Le modèle par défaut n’existe pas", - "error.unexpected": "Une erreur inattendue est survenue ! Activez le mode de débogage pour plus d'informations : https://getkirby.com/docs/reference/system/options/debug", + "error.unexpected": "Une erreur inattendue est survenue ! Activez le mode de débogage pour plus d'informations : https://getkirby.com/docs/reference/system/options/debug", - "error.user.changeEmail.permission": "Vous n’êtes pas autorisé à modifier le courriel de l’utilisateur « {name} »", - "error.user.changeLanguage.permission": "Vous n’êtes pas autorisé à changer la langue de l’utilisateur « {name} »", - "error.user.changeName.permission": "Vous n’êtes pas autorisé à modifier le nom de l’utilisateur « {name} »", - "error.user.changePassword.permission": "Vous n’êtes pas autorisé à changer le mot de passe de l’utilisateur « {name} »", + "error.user.changeEmail.permission": "Vous n’êtes pas autorisé à modifier le courriel de l’utilisateur « {name} »", + "error.user.changeLanguage.permission": "Vous n’êtes pas autorisé à changer la langue de l’utilisateur « {name} »", + "error.user.changeName.permission": "Vous n’êtes pas autorisé à modifier le nom de l’utilisateur « {name} »", + "error.user.changePassword.permission": "Vous n’êtes pas autorisé à changer le mot de passe de l’utilisateur « {name} »", "error.user.changeRole.lastAdmin": "Le rôle du dernier administrateur ne peut être modifié", - "error.user.changeRole.permission": "Vous n’êtes pas autorisé à changer le rôle de l’utilisateur « {name} »", + "error.user.changeRole.permission": "Vous n’êtes pas autorisé à changer le rôle de l’utilisateur « {name} »", "error.user.changeRole.toAdmin": "Vous n’êtes pas autorisé à attribuer le rôle d’administrateur aux utilisateurs", "error.user.create.permission": "Vous n’êtes pas autorisé à créer cet utilisateur", - "error.user.delete": "L’utilisateur « {name} » ne peut être supprimé", + "error.user.delete": "L’utilisateur « {name} » ne peut être supprimé", "error.user.delete.lastAdmin": "Le dernier administrateur ne peut être supprimé", "error.user.delete.lastUser": "Le dernier utilisateur ne peut être supprimé", - "error.user.delete.permission": "Vous n’êtes pas autorisé à supprimer l’utilisateur « {name} »", - "error.user.duplicate": "Un utilisateur avec le courriel « {email} » existe déjà", + "error.user.delete.permission": "Vous n’êtes pas autorisé à supprimer l’utilisateur « {name} »", + "error.user.duplicate": "Un utilisateur avec le courriel « {email} » existe déjà", "error.user.email.invalid": "Veuillez saisir un courriel valide", "error.user.language.invalid": "Veuillez saisir une langue valide", - "error.user.notFound": "L’utilisateur « {name} » n’a pu être trouvé", + "error.user.notFound": "L’utilisateur « {name} » n’a pu être trouvé", "error.user.password.invalid": "Veuillez saisir un mot de passe valide. Les mots de passe doivent comporter au moins 8 caractères.", "error.user.password.notSame": "Les mots de passe ne sont pas identiques", "error.user.password.undefined": "Cet utilisateur n’a pas de mot de passe", "error.user.password.wrong": "Mot de passe incorrect", "error.user.role.invalid": "Veuillez saisir un rôle valide", "error.user.undefined": "L’utilisateur n’a pu être trouvé", - "error.user.update.permission": "Vous n’êtes pas autorisé à modifier l’utilisateur « {name} »", + "error.user.update.permission": "Vous n’êtes pas autorisé à modifier l’utilisateur « {name} »", "error.validation.accepted": "Veuillez confirmer", "error.validation.alpha": "Veuillez saisir uniquement des caractères alphabétiques minuscules", "error.validation.alphanum": "Veuillez ne saisir que des minuscules de a à z et des chiffres de 0 à 9", - "error.validation.between": "Veuillez saisir une valeur entre « {min} » et « {max} »", + "error.validation.between": "Veuillez saisir une valeur entre « {min} » et « {max} »", "error.validation.boolean": "Veuillez confirmer ou refuser", - "error.validation.contains": "Veuillez saisir une valeur contenant « {needle} »", + "error.validation.contains": "Veuillez saisir une valeur contenant « {needle} »", "error.validation.date": "Veuillez saisir une date valide", "error.validation.date.after": "Veuillez saisir une date après {date}", "error.validation.date.before": "Veuillez saisir une date avant {date}", "error.validation.date.between": "Veuillez saisir une date entre {min} et {max}", "error.validation.denied": "Veuillez refuser", - "error.validation.different": "La valeur ne doit pas être « {other} »", + "error.validation.different": "La valeur ne doit pas être « {other} »", "error.validation.email": "Veuillez saisir un courriel valide", - "error.validation.endswith": "La valeur doit se terminer par « {end} »", + "error.validation.endswith": "La valeur doit se terminer par « {end} »", "error.validation.filename": "Veuillez saisir un nom de fichier valide", "error.validation.in": "Veuillez saisir l’un des éléments suivants: ({in})", "error.validation.integer": "Veuillez saisir un entier valide", @@ -210,14 +213,14 @@ "error.validation.minlength": "Veuillez saisir une valeur plus longue (min. {min} caractères)", "error.validation.minwords": "Veuillez saisir au moins {min} mot(s)", "error.validation.more": "Veuillez saisir une valeur supérieure à {min}", - "error.validation.notcontains": "Veuillez saisir une valeur ne contenant pas « {needle} »", + "error.validation.notcontains": "Veuillez saisir une valeur ne contenant pas « {needle} »", "error.validation.notin": "Veuillez ne saisir aucun des éléments suivants: ({notIn})", "error.validation.option": "Veuillez sélectionner une option valide", "error.validation.num": "Veuillez saisir un nombre valide", "error.validation.required": "Veuillez saisir quelque chose", - "error.validation.same": "Veuillez saisir « {other} »", - "error.validation.size": "La grandeur de la valeur doit être « {size} »", - "error.validation.startswith": "La valeur doit commencer par « {start} »", + "error.validation.same": "Veuillez saisir « {other} »", + "error.validation.size": "La grandeur de la valeur doit être « {size} »", + "error.validation.startswith": "La valeur doit commencer par « {start} »", "error.validation.time": "Veuillez saisir une heure valide", "error.validation.time.after": "Veuillez entrer une heure après {time}", "error.validation.time.before": "Veuillez entrer une heure avant {time}", @@ -232,9 +235,9 @@ "field.blocks.code.name": "Code", "field.blocks.code.language": "Langue", "field.blocks.code.placeholder": "Votre code…", - "field.blocks.delete.confirm": "Voulez-vous vraiment supprimer ce bloc ?", - "field.blocks.delete.confirm.all": "Voulez-vous vraiment supprimer tous les blocs ?", - "field.blocks.delete.confirm.selected": "Voulez-vous vraiment supprimer les blocs sélectionnés ?", + "field.blocks.delete.confirm": "Voulez-vous vraiment supprimer ce bloc ?", + "field.blocks.delete.confirm.all": "Voulez-vous vraiment supprimer tous les blocs ?", + "field.blocks.delete.confirm.selected": "Voulez-vous vraiment supprimer les blocs sélectionnés ?", "field.blocks.empty": "Pas encore de blocs", "field.blocks.fieldsets.label": "Veuillez sélectionner un type de bloc…", "field.blocks.fieldsets.paste": "Presser {{ shortcut }} pour coller/importer des blocks depuis votre presse-papier", @@ -275,17 +278,17 @@ "field.files.empty": "Pas encore de fichier sélectionné", "field.layout.delete": "Supprimer cette disposition", - "field.layout.delete.confirm": "Voulez-vous vraiment supprimer cette disposition ?", + "field.layout.delete.confirm": "Voulez-vous vraiment supprimer cette disposition ?", "field.layout.empty": "Pas encore de rangées", "field.layout.select": "Choisir une disposition", "field.pages.empty": "Pas encore de page sélectionnée", - "field.structure.delete.confirm": "Voulez-vous vraiment supprimer cette ligne?", + "field.structure.delete.confirm": "Voulez-vous vraiment supprimer cette ligne ?", "field.structure.empty": "Pas encore d’entrée", "field.users.empty": "Pas encore d’utilisateur sélectionné", "file.blueprint": "Ce fichier n’a pas encore de blueprint. Vous pouvez en définir les paramètres dans /site/blueprints/files/{blueprint}.yml", - "file.delete.confirm": "Voulez-vous vraiment supprimer
{filename} ?", + "file.delete.confirm": "Voulez-vous vraiment supprimer
{filename} ?", "file.sort": "Modifier la position", "files": "Fichiers", @@ -294,6 +297,7 @@ "hide": "Masquer", "hour": "Heure", "import": "Importer", + "info": "Info", "insert": "Insérer", "insert.after": "Insérer après", "insert.before": "Insérer avant", @@ -315,9 +319,9 @@ "language": "Langue", "language.code": "Code", "language.convert": "Choisir comme langue par défaut", - "language.convert.confirm": "

Souhaitez-vous vraiment convertir {name} vers la langue par défaut ? Cette action ne peut pas être annulée.

Si {name} a un contenu non traduit, il n’y aura plus de solution de secours possible et certaines parties de votre site pourraient être vides.

", + "language.convert.confirm": "

Souhaitez-vous vraiment convertir {name} vers la langue par défaut ? Cette action ne peut pas être annulée.

Si {name} a un contenu non traduit, il n’y aura plus de solution de secours possible et certaines parties de votre site pourraient être vides.

", "language.create": "Ajouter une nouvelle langue", - "language.delete.confirm": "Voulez-vous vraiment supprimer la langue {name}, ainsi que toutes ses traductions ? Cette action ne peut être annulée !", + "language.delete.confirm": "Voulez-vous vraiment supprimer la langue {name}, ainsi que toutes ses traductions ? Cette action ne peut être annulée !", "language.deleted": "La langue a été supprimée", "language.direction": "Sens de lecture", "language.direction.ltr": "De gauche à droite", @@ -336,10 +340,12 @@ "license": "Licence", "license.buy": "Acheter une licence", "license.register": "S’enregistrer", + "license.manage": "Gérer vos licences", "license.register.help": "Vous avez reçu votre numéro de licence par courriel après l'achat. Veuillez le copier et le coller ici pour l'enregistrer.", "license.register.label": "Veuillez saisir votre numéro de licence", "license.register.success": "Merci pour votre soutien à Kirby", "license.unregistered": "Ceci est une démo non enregistrée de Kirby", + "license.unregistered.label": "Non enregistré", "link": "Lien", "link.text": "Texte du lien", @@ -354,7 +360,7 @@ "lock.unlock": "Déverrouiller", "lock.isUnlocked": "Vos modifications non enregistrées ont été écrasées pas un autre utilisateur. Vous pouvez télécharger vos modifications pour les fusionner manuellement.", - "login": "Se connecter", + "login": "Connexion", "login.code.label.login": "Code de connexion", "login.code.label.password-reset": "Code de réinitialisation du mot de passe", "login.code.placeholder.email": "000 000", @@ -367,7 +373,7 @@ "login.reset": "Réinitialiser le mot de passe", "login.toggleText.code.email": "Se connecter par courriel", "login.toggleText.code.email-password": "Se connecter avec un mot de passe", - "login.toggleText.password-reset.email": "Mot de passe oublié ?", + "login.toggleText.password-reset.email": "Mot de passe oublié ?", "login.toggleText.password-reset.email-password": "← Retour à la connexion", "logout": "Se déconnecter", @@ -414,7 +420,7 @@ "page.changeStatus.position": "Veuillez sélectionner une position", "page.changeStatus.select": "Sélectionner un nouveau statut", "page.changeTemplate": "Changer de modèle", - "page.delete.confirm": "Voulez-vous vraiment supprimer {title} ?", + "page.delete.confirm": "Voulez-vous vraiment supprimer {title} ?", "page.delete.confirm.subpages": "Cette page contient des sous-pages.
Toutes les sous-pages seront également supprimées.", "page.delete.confirm.title": "Veuillez saisir le titre de la page pour confirmer", "page.draft.create": "Créer un brouillon", @@ -450,7 +456,7 @@ "replace": "Remplacer", "retry": "Essayer à nouveau", "revert": "Revenir", - "revert.confirm": "Voulez-vous vraiment supprimer toutes les modifications non-enregistrées ?", + "revert.confirm": "Voulez-vous vraiment supprimer toutes les modifications non-enregistrées ?", "role": "Rôle", "role.admin.description": "L’administrateur dispose de tous les droits", @@ -469,20 +475,28 @@ "section.required": "Cette section est obligatoire", + "security": "Sécurité", "select": "Sélectionner", + "server": "Serveur", "settings": "Paramètres", "show": "Afficher", + "site.blueprint": "Ce site n’a pas encore de blueprint. Vous pouvez en définir les paramètres dans /site/blueprints/site.yml", "size": "Poids", "slug": "Identifiant de l’URL", "sort": "Trier", + + "stats.empty": "Aucun rapport", + "system.issues.content": "Le dossier content semble exposé", + "system.issues.debug": "Le débogage doit être désactivé en production", + "system.issues.git": "Le dossier .git semble exposé", + "system.issues.https": "Nous recommandons HTTPS pour tous vos sites", + "system.issues.kirby": "Le dossier kirby semble exposé", + "system.issues.site": "Le dossier site semble exposé", + "title": "Titre", "template": "Modèle", "today": "Aujourd’hui", - "server": "Serveur", - - "site.blueprint": "Ce site n’a pas encore de blueprint. Vous pouvez en définir les paramètres dans /site/blueprints/site.yml", - "toolbar.button.code": "Code", "toolbar.button.bold": "Gras", "toolbar.button.email": "Courriel", @@ -539,7 +553,7 @@ "user.changeRole.select": "Sélectionner un nouveau rôle", "user.create": "Ajouter un nouvel utilisateur", "user.delete": "Supprimer cet utilisateur", - "user.delete.confirm": "Voulez-vous vraiment supprimer
{email}?", + "user.delete.confirm": "Voulez-vous vraiment supprimer
{email} ?", "users": "Utilisateurs", diff --git a/kirby/i18n/translations/hu.json b/kirby/i18n/translations/hu.json index 8cfe260..aee0b7e 100644 --- a/kirby/i18n/translations/hu.json +++ b/kirby/i18n/translations/hu.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@pelda.hu", + "entries": "Entries", + "entry": "Entry", + "environment": "Környezet", "error.access.code": "Érvénytelen kód", @@ -294,6 +297,7 @@ "hide": "Elrejtés", "hour": "Óra", "import": "Importálás", + "info": "Info", "insert": "Beilleszt", "insert.after": "Beszúrás mögé", "insert.before": "Beszúrás elé", @@ -336,10 +340,12 @@ "license": "Kirby licenc", "license.buy": "Licenc vásárlása", "license.register": "Regisztráció", + "license.manage": "Manage your licenses", "license.register.help": "A vásárlás után emailben küldjük el a licenc-kódot. Regisztrációhoz másold ide a kapott kódot.", "license.register.label": "Kérlek írd be a licenc-kódot", "license.register.success": "Köszönjük, hogy támogatod a Kirby-t", "license.unregistered": "Jelenleg a Kirby nem regisztrált próbaverzióját használod", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Link szövege", @@ -469,20 +475,28 @@ "section.required": "Ez a szakasz kötelező", + "security": "Security", "select": "Kiválasztás", + "server": "Szerver", "settings": "Beállítások", "show": "Mutat", + "site.blueprint": "Ehhez a weblaphoz még nem tartozik oldalsablon. Itt hozhatod létre: /site/blueprints/site.yml", "size": "Méret", "slug": "URL n\u00e9v", "sort": "Rendezés", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Cím", "template": "Sablon", "today": "Ma", - "server": "Szerver", - - "site.blueprint": "Ehhez a weblaphoz még nem tartozik oldalsablon. Itt hozhatod létre: /site/blueprints/site.yml", - "toolbar.button.code": "Kód", "toolbar.button.bold": "F\u00e9lk\u00f6v\u00e9r sz\u00f6veg", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/id.json b/kirby/i18n/translations/id.json index c9404bd..596e92a 100644 --- a/kirby/i18n/translations/id.json +++ b/kirby/i18n/translations/id.json @@ -49,6 +49,9 @@ "email": "Surel", "email.placeholder": "surel@contoh.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Kode tidak valid", @@ -294,6 +297,7 @@ "hide": "Sembunyikan", "hour": "Jam", "import": "Import", + "info": "Info", "insert": "Sisipkan", "insert.after": "Sisipkan setelah", "insert.before": "Sisipkan sebelum", @@ -336,10 +340,12 @@ "license": "Lisensi Kirby", "license.buy": "Beli lisensi", "license.register": "Daftar", + "license.manage": "Manage your licenses", "license.register.help": "Anda menerima kode lisensi via surel setelah pembelian. Salin dan tempel kode tersebut untuk mendaftarkan.", "license.register.label": "Masukkan kode lisensi Anda", "license.register.success": "Terima kasih atas dukungan untuk Kirby", "license.unregistered": "Ini adalah demo tidak diregistrasi dari Kirby", + "license.unregistered.label": "Unregistered", "link": "Tautan", "link.text": "Teks tautan", @@ -469,20 +475,28 @@ "section.required": "Bagian ini wajib", + "security": "Security", "select": "Pilih", + "server": "Server", "settings": "Pengaturan", "show": "Tampilkan", + "site.blueprint": "Situs ini belum memiliki cetak biru. Anda dapat mendefinisikannya di /site/blueprints/site.yml", "size": "Ukuran", "slug": "Akhiran URL", "sort": "Urutkan", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Judul", "template": "Templat", "today": "Hari ini", - "server": "Server", - - "site.blueprint": "Situs ini belum memiliki cetak biru. Anda dapat mendefinisikannya di /site/blueprints/site.yml", - "toolbar.button.code": "Kode", "toolbar.button.bold": "Tebal", "toolbar.button.email": "Surel", diff --git a/kirby/i18n/translations/is_IS.json b/kirby/i18n/translations/is_IS.json index ee26848..59fab9b 100644 --- a/kirby/i18n/translations/is_IS.json +++ b/kirby/i18n/translations/is_IS.json @@ -49,6 +49,9 @@ "email": "Netfang", "email.placeholder": "nafn@netfang.is", + "entries": "Entries", + "entry": "Entry", + "environment": "Umhverfi", "error.access.code": "Ógildur kóði", @@ -294,6 +297,7 @@ "hide": "Fela", "hour": "Klukkustund", "import": "Hlaða inn", + "info": "Info", "insert": "Setja inn", "insert.after": "Setja eftir", "insert.before": "Setja fyrir", @@ -336,10 +340,12 @@ "license": "Leyfi", "license.buy": "Kaupa leyfi", "license.register": "Skr\u00E1 Kirby", + "license.manage": "Manage your licenses", "license.register.help": "Þú fékkst sendan tölvupóst með leyfiskóðanum þegar þú keyptir leyfi. Vinsamlegast afritaðu hann og settu hann hingað til að skrá þig.", "license.register.label": "Vinsamlegast settu inn leyfiskóðan", "license.register.success": "Þakka þér fyrir að velja Kirby", "license.unregistered": "Þetta er óskráð prufueintak af Kirby", + "license.unregistered.label": "Unregistered", "link": "Tengill", "link.text": "Tengilstexti", @@ -469,20 +475,28 @@ "section.required": "Þetta svæði er nauðsynlegt", + "security": "Security", "select": "Velja", + "server": "Vefþjónn", "settings": "Stillingar", "show": "Sýna", + "site.blueprint": "Þessi vefur hefur ekki skipan (e. blueprint) ennþá. Þú mátt skilgreina skipanina í /site/blueprints/site.yml", "size": "Stærð", "slug": "Slögg", "sort": "Raða", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titill", "template": "Sniðmát", "today": "Núna", - "server": "Vefþjónn", - - "site.blueprint": "Þessi vefur hefur ekki skipan (e. blueprint) ennþá. Þú mátt skilgreina skipanina í /site/blueprints/site.yml", - "toolbar.button.code": "Kóðasnið", "toolbar.button.bold": "Feitletrun", "toolbar.button.email": "Netfang", diff --git a/kirby/i18n/translations/it.json b/kirby/i18n/translations/it.json index 42d5db4..60151cb 100644 --- a/kirby/i18n/translations/it.json +++ b/kirby/i18n/translations/it.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@esempio.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Ambiente", "error.access.code": "Codice non valido", @@ -294,6 +297,7 @@ "hide": "Nascondi", "hour": "Ora", "import": "Importa", + "info": "Info", "insert": "Inserisci", "insert.after": "Inserisci dopo", "insert.before": "Inserisci prima", @@ -336,10 +340,12 @@ "license": "Licenza di Kirby", "license.buy": "Acquista una licenza", "license.register": "Registra", + "license.manage": "Manage your licenses", "license.register.help": "Hai ricevuto il codice di licenza tramite email dopo l'acquisto. Per favore inseriscilo per registrare Kirby.", "license.register.label": "Inserisci il codice di licenza", "license.register.success": "Ti ringraziamo per aver supportato Kirby", "license.unregistered": "Questa è una versione demo di Kirby non registrata", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Testo del link", @@ -469,20 +475,28 @@ "section.required": "La sezione è obbligatoria", + "security": "Security", "select": "Seleziona", + "server": "Server", "settings": "Impostazioni", "show": "Mostra", + "site.blueprint": "Il sito non ha ancora un \"blueprint\". Puoi impostarne uno in /site/blueprints/site.yml", "size": "Dimensioni", "slug": "URL", "sort": "Ordina", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titolo", "template": "Template", "today": "Oggi", - "server": "Server", - - "site.blueprint": "Il sito non ha ancora un \"blueprint\". Puoi impostarne uno in /site/blueprints/site.yml", - "toolbar.button.code": "Codice", "toolbar.button.bold": "Grassetto", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/ko.json b/kirby/i18n/translations/ko.json index ade311f..3891554 100644 --- a/kirby/i18n/translations/ko.json +++ b/kirby/i18n/translations/ko.json @@ -29,7 +29,7 @@ "days.tue": "\ud654", "days.wed": "\uc218", - "debugging": "디버깅", + "debugging": "디버그", "delete": "\uc0ad\uc81c", "delete.all": "모두 삭제", @@ -49,6 +49,9 @@ "email": "\uc774\uba54\uc77c \uc8fc\uc18c", "email.placeholder": "mail@example.com", + "entries": "항목", + "entry": "항목", + "environment": "구동 환경", "error.access.code": "코드가 올바르지 않습니다.", @@ -294,6 +297,7 @@ "hide": "숨기기", "hour": "시", "import": "가져오기", + "info": "정보", "insert": "\uc0bd\uc785", "insert.after": "뒤에 삽입", "insert.before": "앞에 삽입", @@ -302,15 +306,15 @@ "installation": "설치", "installation.completed": "패널을 설치했습니다.", "installation.disabled": "패널 설치 관리자는 로컬 서버에서 실행하거나 panel.install 옵션을 설정하세요.", - "installation.issues.accounts": "폴더(/site/accounts)에 쓰기 권한이 없습니다.", - "installation.issues.content": "폴더(/content)에 쓰기 권한이 없습니다.", + "installation.issues.accounts": "/site/accounts 폴더의 쓰기 권한을 확인하세요.", + "installation.issues.content": "/content 폴더의 쓰기 권한을 확인하세요.", "installation.issues.curl": "cURL 확장 모듈이 필요합니다.", "installation.issues.headline": "패널을 설치할 수 없습니다.", "installation.issues.mbstring": "MB String 확장 모듈이 필요합니다.", - "installation.issues.media": "폴더(/media)에 쓰기 권한이 없습니다.", + "installation.issues.media": "/media 폴더의 쓰기 권한을 확인하세요.", "installation.issues.php": "PHP 버전이 7 이상인지 확인하세요.", "installation.issues.server": "Kirby를 실행하려면 Apache, Nginx, 또는 Caddy가 필요합니다.", - "installation.issues.sessions": "폴더(/site/sessions)에 쓰기 권한이 없습니다.", + "installation.issues.sessions": "/site/sessions 폴더의 쓰기 권한을 확인하세요.", "language": "\uc5b8\uc5b4", "language.code": "언어 코드", @@ -323,7 +327,7 @@ "language.direction.ltr": "왼쪽에서 오른쪽", "language.direction.rtl": "오른쪽에서 왼쪽", "language.locale": "PHP 로캘 문자열", - "language.locale.warning": "사용자 지정 로캘을 사용 중입니다. 폴더(/site/languages)의 언어 파일을 수정하세요.", + "language.locale.warning": "사용자 지정 로캘을 사용 중입니다. /site/languages 폴더의 언어 파일을 수정하세요.", "language.name": "언어명", "language.updated": "언어를 변경했습니다.", @@ -336,10 +340,12 @@ "license": "라이선스", "license.buy": "라이선스 구매", "license.register": "등록", + "license.manage": "라이선스 관리", "license.register.help": "Kirby를 등록하려면 이메일로 전송받은 라이선스 코드와 이메일 주소를 입력하세요.", "license.register.label": "라이선스 코드를 입력하세요.", "license.register.success": "Kirby와 함께해주셔서 감사합니다.", "license.unregistered": "Kirby가 등록되지 않았습니다.", + "license.unregistered.label": "Kirby가 등록되지 않았습니다.", "link": "\uc77c\ubc18 \ub9c1\ud06c", "link.text": "\ubb38\uc790", @@ -354,7 +360,7 @@ "lock.unlock": "잠금 해제", "lock.isUnlocked": "다른 사용자가 이미 내용을 수정했으므로 현재 내용이 올바르게 저장되지 않았습니다. 저장되지 않은 내용은 다운로드해 수동으로 대치할 수 있습니다.", - "login": "\ub85c\uadf8\uc778", + "login": "로그인", "login.code.label.login": "로그인 코드", "login.code.label.password-reset": "암호 초기화 코드", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "섹션이 필요합니다.", + "security": "보안", "select": "선택", + "server": "서버", "settings": "설정", "show": "보기", + "site.blueprint": "블루프린트(/site/blueprints/site.yml)를 설정하세요.", "size": "크기", "slug": "고유 주소", "sort": "정렬", + + "stats.empty": "관련 기록이 없습니다.", + "system.issues.content": "/content 폴더의 권한을 확인하세요.", + "system.issues.debug": "공개 서버상에서는 디버그 모드를 해제하세요.", + "system.issues.git": "/.git 폴더의 권한을 확인하세요.", + "system.issues.https": "HTTPS를 권장합니다.", + "system.issues.kirby": "/kirby 폴더의 권한을 확인하세요.", + "system.issues.site": "/site 폴더의 권한을 확인하세요.", + "title": "제목", "template": "\ud15c\ud50c\ub9bf", "today": "오늘", - "server": "서버", - - "site.blueprint": "블루프린트(/site/blueprints/site.yml)를 설정하세요.", - "toolbar.button.code": "코드", "toolbar.button.bold": "강조", "toolbar.button.email": "이메일 주소", diff --git a/kirby/i18n/translations/lt.json b/kirby/i18n/translations/lt.json index cccd7f0..8741d72 100644 --- a/kirby/i18n/translations/lt.json +++ b/kirby/i18n/translations/lt.json @@ -49,6 +49,9 @@ "email": "El. paštas", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Neteisinas kodas", @@ -294,6 +297,7 @@ "hide": "Paslėpti", "hour": "Valanda", "import": "Importuoti", + "info": "Info", "insert": "Įterpti", "insert.after": "Įterpti po", "insert.before": "Įterpti prieš", @@ -336,10 +340,12 @@ "license": "Licenzija", "license.buy": "Pirkti licenziją", "license.register": "Registruoti", + "license.manage": "Manage your licenses", "license.register.help": "Licenzijos kodą gavote el. paštu po apmokėjimo. Prašome įterpti čia, kad sistema būtų užregistruota.", "license.register.label": "Prašome įrašyti jūsų licenzijos kodą", "license.register.success": "Ačiū, kad palaikote Kirby", "license.unregistered": "Tai neregistruota Kirby demo versija", + "license.unregistered.label": "Unregistered", "link": "Nuoroda", "link.text": "Nuorodos tekstas", @@ -469,20 +475,28 @@ "section.required": "Sekcija privaloma", + "security": "Security", "select": "Pasirinkti", + "server": "Server", "settings": "Nustatymai", "show": "Rodyti", + "site.blueprint": "Svetainė neturi blueprint. Jūs galite nustatyti jį /site/blueprints/site.yml", "size": "Dydis", "slug": "URL pabaiga", "sort": "Rikiuoti", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Pavadinimas", "template": "Puslapio šablonas", "today": "Šiandien", - "server": "Server", - - "site.blueprint": "Svetainė neturi blueprint. Jūs galite nustatyti jį /site/blueprints/site.yml", - "toolbar.button.code": "Kodas", "toolbar.button.bold": "Bold", "toolbar.button.email": "El. paštas", diff --git a/kirby/i18n/translations/nb.json b/kirby/i18n/translations/nb.json index 19a67a6..a4f9048 100644 --- a/kirby/i18n/translations/nb.json +++ b/kirby/i18n/translations/nb.json @@ -49,6 +49,9 @@ "email": "Epost", "email.placeholder": "epost@eksempel.no", + "entries": "Entries", + "entry": "Entry", + "environment": "Miljø", "error.access.code": "Ugyldig kode", @@ -294,6 +297,7 @@ "hide": "Skjul", "hour": "Tid", "import": "Importer", + "info": "Info", "insert": "Sett Inn", "insert.after": "Sett inn etter", "insert.before": "Sett inn før", @@ -336,10 +340,12 @@ "license": "Kirby lisens", "license.buy": "Kjøp lisens", "license.register": "Registrer", + "license.manage": "Manage your licenses", "license.register.help": "Du skal ha mottatt din lisenskode for kjøpet via e-post. Vennligst kopier og lim inn denne for å registrere deg.", "license.register.label": "Vennligst skriv inn din lisenskode", "license.register.success": "Takk for at du støtter Kirby", "license.unregistered": "Dette er en uregistrert demo av Kirby", + "license.unregistered.label": "Unregistered", "link": "Adresse", "link.text": "Koblingstekst", @@ -469,20 +475,28 @@ "section.required": "Denne seksjonen er påkrevd", + "security": "Security", "select": "Velg", + "server": "Server", "settings": "Innstillinger", "show": "Vis", + "site.blueprint": "Denne siden har ikke en blueprint enda. Du kan definere oppsettet i /site/blueprints/site.yml", "size": "Størrelse", "slug": "URL-appendiks", "sort": "Sortere", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Tittel", "template": "Mal", "today": "I dag", - "server": "Server", - - "site.blueprint": "Denne siden har ikke en blueprint enda. Du kan definere oppsettet i /site/blueprints/site.yml", - "toolbar.button.code": "Kode", "toolbar.button.bold": "Fet tekst", "toolbar.button.email": "Epost", diff --git a/kirby/i18n/translations/nl.json b/kirby/i18n/translations/nl.json index 29b46d2..e9fee94 100644 --- a/kirby/i18n/translations/nl.json +++ b/kirby/i18n/translations/nl.json @@ -49,6 +49,9 @@ "email": "E-mailadres", "email.placeholder": "mail@voorbeeld.nl", + "entries": "Entries", + "entry": "Entry", + "environment": "Omgeving", "error.access.code": "Ongeldige code", @@ -157,7 +160,7 @@ "error.template.default.notFound": "Het standaard template bestaat niet", - "error.unexpected": "An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug", + "error.unexpected": "Een onverwacht fout heeft plaats gevonden! Schakel debug-modus in voor meer informatie: https://getkirby.com/docs/reference/system/options/debug", "error.user.changeEmail.permission": "Je hebt geen rechten om het e-mailadres van gebruiker \"{name}\" te wijzigen", "error.user.changeLanguage.permission": "Je hebt geen rechten om de taal voor gebruiker \"{name}\" te wijzigen", @@ -294,6 +297,7 @@ "hide": "Verberg", "hour": "Uur", "import": "Importeer", + "info": "Info", "insert": "Toevoegen", "insert.after": "Voeg toe na", "insert.before": "Voeg toe voor", @@ -336,10 +340,12 @@ "license": "Licentie", "license.buy": "Koop een licentie", "license.register": "Registreren", + "license.manage": "Manage your licenses", "license.register.help": "Je hebt de licentie via e-mail gekregen nadat je de aankoop hebt gedaan. Kopieer en plak de licentie om te registreren. ", "license.register.label": "Vul je licentie in", "license.register.success": "Bedankt dat je Kirby ondersteunt", "license.unregistered": "Dit is een niet geregistreerde demo van Kirby", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Linktekst", @@ -469,20 +475,28 @@ "section.required": "De sectie is verplicht", + "security": "Security", "select": "Selecteren", + "server": "Server", "settings": "Opties", "show": "Toon", + "site.blueprint": "Deze website heeft nog geen ontwerp. Je kan het ontwerp hier plaatsen/site/blueprints/site.yml", "size": "Grootte", "slug": "URL-toevoeging", "sort": "Sorteren", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titel", "template": "Template", "today": "Vandaag", - "server": "Server", - - "site.blueprint": "Deze website heeft nog geen ontwerp. Je kan het ontwerp hier plaatsen/site/blueprints/site.yml", - "toolbar.button.code": "Code", "toolbar.button.bold": "Dikgedrukte tekst", "toolbar.button.email": "E-mailadres", diff --git a/kirby/i18n/translations/pl.json b/kirby/i18n/translations/pl.json index 688f58a..befaae9 100644 --- a/kirby/i18n/translations/pl.json +++ b/kirby/i18n/translations/pl.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@example.com", + "entries": "Wpisy", + "entry": "Wpis", + "environment": "Środowisko", "error.access.code": "Nieprawidłowy kod", @@ -294,6 +297,7 @@ "hide": "Ukryj", "hour": "Godzina", "import": "Importuj", + "info": "Informacje", "insert": "Wstaw", "insert.after": "Wstaw po", "insert.before": "Wstaw przed", @@ -336,10 +340,12 @@ "license": "Licencja", "license.buy": "Kup licencję", "license.register": "Zarejestruj", + "license.manage": "Zarządzaj swoimi licencjami", "license.register.help": "Po zakupieniu licencji otrzymałaś/-eś mailem klucz. Skopiuj go i wklej tutaj, aby dokonać rejestracji.", "license.register.label": "Wprowadź swój kod licencji", "license.register.success": "Dziękujemy za wspieranie Kirby", "license.unregistered": "To jest niezarejestrowana wersja demonstracyjna Kirby", + "license.unregistered.label": "Niezarejestrowane", "link": "Link", "link.text": "Tekst linku", @@ -354,7 +360,7 @@ "lock.unlock": "Odblokuj", "lock.isUnlocked": "Twoje niezapisane zmiany zostały nadpisane przez innego użytkownika. Możesz pobrać swoje zmiany, by scalić je ręcznie.", - "login": "Zaloguj", + "login": "Zaloguj się", "login.code.label.login": "Kod logowania się", "login.code.label.password-reset": "Kod resetowania hasła", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "Sekcja jest wymagana", + "security": "Bezpieczeństwo", "select": "Wybierz", + "server": "Serwer", "settings": "Ustawienia", "show": "Pokaż", + "site.blueprint": "Ta strona nie ma jeszcze wzorca. Możesz go zdefiniować w /site/blueprints/site.yml", "size": "Rozmiar", "slug": "Końcówka URL", "sort": "Sortuj", + + "stats.empty": "Brak raportów", + "system.issues.content": "Zdaje się, że folder „content” jest wystawiony na publiczny dostęp", + "system.issues.debug": "Debugowanie musi być wyłączone w środowisku produkcyjnym", + "system.issues.git": "Zdaje się, że folder „.git” jest wystawiony na publiczny dostęp", + "system.issues.https": "Zalecamy HTTPS dla wszystkich Twoich witryn", + "system.issues.kirby": "Zdaje się, że folder „kirby” jest wystawiony na publiczny dostęp", + "system.issues.site": "Zdaje się, że folder „site” jest wystawiony na publiczny dostęp", + "title": "Tytuł", "template": "Szablon", "today": "Dzisiaj", - "server": "Serwer", - - "site.blueprint": "Ta strona nie ma jeszcze wzorca. Możesz go zdefiniować w /site/blueprints/site.yml", - "toolbar.button.code": "Kod", "toolbar.button.bold": "Pogrubienie", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/pt_BR.json b/kirby/i18n/translations/pt_BR.json index f18cdae..6181eca 100644 --- a/kirby/i18n/translations/pt_BR.json +++ b/kirby/i18n/translations/pt_BR.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@exemplo.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Ambiente", "error.access.code": "Código inválido", @@ -294,6 +297,7 @@ "hide": "Ocultar", "hour": "Hora", "import": "Importar", + "info": "Info", "insert": "Inserir", "insert.after": "Inserir após", "insert.before": "Inserir antes", @@ -336,10 +340,12 @@ "license": "Licen\u00e7a do Kirby ", "license.buy": "Comprar licença", "license.register": "Registrar", + "license.manage": "Manage your licenses", "license.register.help": "Você recebeu o código da sua licença por email ao efetuar sua compra. Por favor, copie e cole o código para completar seu registro.", "license.register.label": "Por favor, digite o código da sua licença", "license.register.success": "Obrigado por apoiar o Kirby", "license.unregistered": "Esta é uma cópia de demonstração não registrada do Kirby", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Texto do link", @@ -469,20 +475,28 @@ "section.required": "Esta seção é obrigatória", + "security": "Security", "select": "Selecionar", + "server": "Servidor", "settings": "Configurações", "show": "Mostrar", + "site.blueprint": "Este site não tem planta. Você pode definir sua planta em /site/blueprints/site.yml", "size": "Tamanho", "slug": "Anexo de URL", "sort": "Ordenar", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Título", "template": "Tema", "today": "Hoje", - "server": "Servidor", - - "site.blueprint": "Este site não tem planta. Você pode definir sua planta em /site/blueprints/site.yml", - "toolbar.button.code": "Código", "toolbar.button.bold": "Negrito", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/pt_PT.json b/kirby/i18n/translations/pt_PT.json index 026c581..a2a20f1 100644 --- a/kirby/i18n/translations/pt_PT.json +++ b/kirby/i18n/translations/pt_PT.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@exemplo.pt", + "entries": "Entries", + "entry": "Entry", + "environment": "Ambiente", "error.access.code": "Código inválido", @@ -294,6 +297,7 @@ "hide": "Ocultar", "hour": "Hora", "import": "Importar", + "info": "Info", "insert": "Inserir", "insert.after": "Inserir após", "insert.before": "Inserir antes", @@ -336,10 +340,12 @@ "license": "Licen\u00e7a do Kirby ", "license.buy": "Comprar uma licença", "license.register": "Registrar", + "license.manage": "Manage your licenses", "license.register.help": "Recebeu o código da sua licença por email após a compra. Por favor, copie e cole-o para completar o registro.", "license.register.label": "Por favor, digite o código da sua licença", "license.register.success": "Obrigado por apoiar o Kirby", "license.unregistered": "Esta é uma demonstração não registrada do Kirby", + "license.unregistered.label": "Unregistered", "link": "Link", "link.text": "Texto do link", @@ -469,20 +475,28 @@ "section.required": "Esta seção é necessária", + "security": "Security", "select": "Selecionar", + "server": "Servidor", "settings": "Configurações", "show": "Mostrar", + "site.blueprint": "Este site não tem planta. Você pode definir sua planta em /site/blueprints/site.yml", "size": "Tamanho", "slug": "URL", "sort": "Ordenar", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Título", "template": "Tema", "today": "Hoje", - "server": "Servidor", - - "site.blueprint": "Este site não tem planta. Você pode definir sua planta em /site/blueprints/site.yml", - "toolbar.button.code": "Código", "toolbar.button.bold": "Negrito", "toolbar.button.email": "Email", diff --git a/kirby/i18n/translations/ru.json b/kirby/i18n/translations/ru.json index eab4f2b..c38fb9d 100644 --- a/kirby/i18n/translations/ru.json +++ b/kirby/i18n/translations/ru.json @@ -49,6 +49,9 @@ "email": "Email", "email.placeholder": "mail@example.com", + "entries": "Записи", + "entry": "Запись", + "environment": "Среда", "error.access.code": "Неверный код", @@ -174,7 +177,7 @@ "error.user.duplicate": "Пользователь с Email \"{email}\" уже есть", "error.user.email.invalid": "Пожалуйста, введите правильный адрес эл. почты", "error.user.language.invalid": "Введите правильный язык", - "error.user.notFound": "\u0410\u043a\u043a\u0430\u0443\u043d\u0442 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d", + "error.user.notFound": "Пользователь \"{name}\" не найден", "error.user.password.invalid": "Пожалуйста, введите правильный пароль. Он должен состоять минимум из 8 символов.", "error.user.password.notSame": "\u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c", "error.user.password.undefined": "У пользователя нет пароля", @@ -235,11 +238,11 @@ "field.blocks.delete.confirm": "Вы действительно хотите удалить этот блок?", "field.blocks.delete.confirm.all": "Вы действительно хотите удалить все блоки?", "field.blocks.delete.confirm.selected": "Вы действительно хотите удалить эти блоки?", - "field.blocks.empty": "Еще нет блоков", + "field.blocks.empty": "Блоков нет", "field.blocks.fieldsets.label": "Пожалуйста, выберите тип блока…", "field.blocks.fieldsets.paste": "Нажмите {{ shortcut }} чтобы вставить/импортировать блоки из буфера памяти", "field.blocks.gallery.name": "Галерея", - "field.blocks.gallery.images.empty": "Еще нет изображений", + "field.blocks.gallery.images.empty": "Изображений нет", "field.blocks.gallery.images.label": "Изображения", "field.blocks.heading.level": "Уровень", "field.blocks.heading.name": "Заголовок", @@ -272,17 +275,17 @@ "field.blocks.video.url.label": "Ссылка на видео", "field.blocks.video.url.placeholder": "https://youtube.com/?v=", - "field.files.empty": "Еще не выбраны файлы", + "field.files.empty": "Файлы не выбраны", "field.layout.delete": "Удалить разметку", "field.layout.delete.confirm": "Вы действительно хотите удалить эту разметку?", - "field.layout.empty": "Еще нет строк", + "field.layout.empty": "Строк нет", "field.layout.select": "Выберите разметку", - "field.pages.empty": "Еще не выбраны страницы", + "field.pages.empty": "Страницы не выбраны", "field.structure.delete.confirm": "Вы точно хотите удалить эту запись?", - "field.structure.empty": "Еще нет записей", - "field.users.empty": "Еще нет пользователей", + "field.structure.empty": "Записей нет", + "field.users.empty": "Пользователей нет", "file.blueprint": "У файла пока нет разметки. Вы можете определить новые секции и поля разметки в /site/blueprints/files/{blueprint}.yml", "file.delete.confirm": "Вы точно хотите удалить файл
{filename}?", @@ -294,6 +297,7 @@ "hide": "Скрыть", "hour": "Час", "import": "Импортировать", + "info": "Информация", "insert": "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044c", "insert.after": "Вставить ниже", "insert.before": "Вставить выше", @@ -329,17 +333,19 @@ "languages": "Языки", "languages.default": "Главный язык", - "languages.empty": "Еще нет языков", + "languages.empty": "Языков нет", "languages.secondary": "Дополнительные языки", - "languages.secondary.empty": "Еще нет дополнительных языков", + "languages.secondary.empty": "Дополнительных языков нет", "license": "Лицензия", "license.buy": "Купить лицензию", "license.register": "Зарегистрировать", + "license.manage": "Управление лицензиями", "license.register.help": "После покупки вы получили по эл. почте код лицензии. Пожалуйста скопируйте и вставьте сюда чтобы зарегистрировать.", "license.register.label": "Пожалуйста вставьте код лицензии", "license.register.success": "Спасибо за поддержку Kirby", "license.unregistered": "Это незарегистрированная версия Kirby", + "license.unregistered.label": "Не зарегистрировано", "link": "\u0421\u0441\u044b\u043b\u043a\u0430", "link.text": "\u0422\u0435\u043a\u0441\u0442 \u0441\u0441\u044b\u043b\u043a\u0438", @@ -347,7 +353,7 @@ "loading": "Загрузка", "lock.unsaved": "Несохраненные изменения", - "lock.unsaved.empty": "Больше нет несохраненных изменений", + "lock.unsaved.empty": "Несохраненных изменений больше нет", "lock.isLocked": "Несохраненные изменения пользователя {email}", "lock.file.isLocked": "В данный момент этот файл редактирует {email}, поэтому его нельзя изменить.", "lock.page.isLocked": "В данный момент эту страницу редактирует {email}, поэтому его нельзя изменить.", @@ -359,9 +365,9 @@ "login.code.label.password-reset": "Код для сброса пароля", "login.code.placeholder.email": "000 000", "login.code.text.email": "Если ваш Email уже зарегистрирован, запрашиваемый код был отправлен на него.", - "login.email.login.body": "Привет, {user.nameOrEmail}!\n\nНедавно вы запросили код для входа в Панель управления {site}.\nСледующий код входа будет действителен в течение {timeout} минут:\n\n{code}\n\nЕсли вы не запрашивали код для входа, проигнорируйте это письмо или обратитесь к администратору, если у вас есть вопросы.\nВ целях безопасности НЕ ПЕРЕСЫЛАЙТЕ это письмо.", + "login.email.login.body": "Привет, {user.nameOrEmail}!\n\nНедавно вы запросили код для входа на «{site}».\nСледующий код входа будет действителен в течение {timeout} минут:\n\n{code}\n\nЕсли вы не запрашивали код для входа, проигнорируйте это письмо или обратитесь к администратору, если у вас есть вопросы.\nВ целях безопасности НЕ ПЕРЕСЫЛАЙТЕ это письмо.", "login.email.login.subject": "Ваш код для входа", - "login.email.password-reset.body": "Привет, {user.nameOrEmail}!\n\nНедавно вы запросили сброс пароля для входа в Панель управления {site}.\nСледующий код входа будет действителен в течение {timeout} минут:\n\n{code}\n\nЕсли вы не запрашивали сброс пароля, проигнорируйте это письмо или обратитесь к администратору, если у вас есть вопросы.\nВ целях безопасности НЕ ПЕРЕСЫЛАЙТЕ это письмо.", + "login.email.password-reset.body": "Привет, {user.nameOrEmail}!\n\nНедавно вы запросили сброс пароля для входа на «{site}».\nСледующий код входа будет действителен в течение {timeout} минут:\n\n{code}\n\nЕсли вы не запрашивали сброс пароля, проигнорируйте это письмо или обратитесь к администратору, если у вас есть вопросы.\nВ целях безопасности НЕ ПЕРЕСЫЛАЙТЕ это письмо.", "login.email.password-reset.subject": "Ваш код для сброса пароля", "login.remember": "Сохранять вход активным", "login.reset": "Сбросить пароль", @@ -400,7 +406,7 @@ "open": "Открыть", "open.newWindow": "Открывать в новом окне", "options": "Опции", - "options.none": "Нет параметров", + "options.none": "Параметров нет", "orientation": "Ориентация", "orientation.landscape": "Горизонтальная", @@ -418,7 +424,7 @@ "page.delete.confirm.subpages": "У этой страницы есть внутренние страницы.
Все внутренние страницы так же будут удалены.", "page.delete.confirm.title": "Напишите название страницы, чтобы подтвердить", "page.draft.create": "Создать черновик", - "page.duplicate.appendix": "Скопировать", + "page.duplicate.appendix": "(копия)", "page.duplicate.files": "Копировать файлы", "page.duplicate.pages": "Копировать страницы", "page.sort": "Изменить позицию", @@ -431,7 +437,7 @@ "page.status.unlisted.description": "Страница доступна только по URL", "pages": "Страницы", - "pages.empty": "Еще нет страниц", + "pages.empty": "Страниц нет", "pages.status.draft": "Черновики", "pages.status.listed": "Опубликовано", "pages.status.unlisted": "Скрытая", @@ -456,7 +462,7 @@ "role.admin.description": "Администратор имеет все права", "role.admin.title": "Администратор", "role.all": "Все", - "role.empty": "Нет пользователей с такой ролью", + "role.empty": "Пользователей с такой ролью нет", "role.description.placeholder": "Без описания", "role.nobody.description": "Эта роль применяется если у пользователя нет никаких прав", "role.nobody.title": "Никто", @@ -469,20 +475,28 @@ "section.required": "Секция обязательна", + "security": "Безопасность", "select": "Выбрать", + "server": "Сервер", "settings": "Настройка", "show": "Показать", + "site.blueprint": "У сайта пока нет разметки. Вы можете определить новые секции и поля разметки в /site/blueprints/site.yml", "size": "Размер", "slug": "Понятная ссылка", "sort": "Сортировать", + + "stats.empty": "Статистики нет", + "system.issues.content": "Похоже, к папке content есть несанкционированный доступ", + "system.issues.debug": "Включен режим отладки (debugging). Используйте его только при разработке.", + "system.issues.git": "Похоже, к папке .git есть несанкционированный доступ", + "system.issues.https": "Рекомендуется использовать HTTPS на всех сайтах", + "system.issues.kirby": "Похоже, к папке kirby есть несанкционированный доступ", + "system.issues.site": "Похоже, к папке site есть несанкционированный доступ", + "title": "Название", "template": "\u0428\u0430\u0431\u043b\u043e\u043d", "today": "Сегодня", - "server": "Сервер", - - "site.blueprint": "У сайта пока нет разметки. Вы можете определить новые секции и поля разметки в /site/blueprints/site.yml", - "toolbar.button.code": "Код", "toolbar.button.bold": "\u0416\u0438\u0440\u043d\u044b\u0439 \u0448\u0440\u0438\u0444\u0442", "toolbar.button.email": "Email", @@ -510,9 +524,9 @@ "translation.locale": "ru_RU", "upload": "Закачать", - "upload.error.cantMove": "Загруженный файл не может быть перемещен", + "upload.error.cantMove": "Не удается переместить загруженный файл", "upload.error.cantWrite": "Не получилось записать файл на диск", - "upload.error.default": "Не получилось загрузить файл", + "upload.error.default": "Не удалось загрузить файл", "upload.error.extension": "Загрузка файла не удалась из за расширения", "upload.error.formSize": "Загруженный файл больше чем MAX_FILE_SIZE настройка в форме", "upload.error.iniPostSize": "Загружаемый файл больше чем post_max_size настройка в php.ini", diff --git a/kirby/i18n/translations/sk.json b/kirby/i18n/translations/sk.json index 023c27e..a7d53d2 100644 --- a/kirby/i18n/translations/sk.json +++ b/kirby/i18n/translations/sk.json @@ -49,6 +49,9 @@ "email": "E-mail", "email.placeholder": "mail@example.com", + "entries": "Entries", + "entry": "Entry", + "environment": "Environment", "error.access.code": "Neplatný kód", @@ -294,6 +297,7 @@ "hide": "Hide", "hour": "Hodina", "import": "Import", + "info": "Info", "insert": "Vložiť", "insert.after": "Insert after", "insert.before": "Insert before", @@ -336,10 +340,12 @@ "license": "Licencia", "license.buy": "Zakúpiť licenciu", "license.register": "Registrovať", + "license.manage": "Manage your licenses", "license.register.help": "Licenčný kód vám bol doručený e-mailom po úspešnom nákupe. Prosím, skopírujte a prilepte ho na uskutočnenie registrácie.", "license.register.label": "Prosím, zadajte váš licenčný kód", "license.register.success": "Ďakujeme za vašu podporu Kirby", "license.unregistered": "Toto je neregistrované demo Kirby", + "license.unregistered.label": "Unregistered", "link": "Odkaz", "link.text": "Text odkazu", @@ -469,20 +475,28 @@ "section.required": "The section is required", + "security": "Security", "select": "Zvoliť", + "server": "Server", "settings": "Nastavenia", "show": "Show", + "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", "size": "Veľkosť", "slug": "URL appendix", "sort": "Zoradiť", + + "stats.empty": "No reports", + "system.issues.content": "The content folder seems to be exposed", + "system.issues.debug": "Debugging must be turned off in production", + "system.issues.git": "The .git folder seems to be exposed", + "system.issues.https": "We recommend HTTPS for all your sites", + "system.issues.kirby": "The kirby folder seems to be exposed", + "system.issues.site": "The site folder seems to be exposed", + "title": "Titulok", "template": "Šablóna", "today": "Dnes", - "server": "Server", - - "site.blueprint": "The site has no blueprint yet. You can define the setup in /site/blueprints/site.yml", - "toolbar.button.code": "Kód", "toolbar.button.bold": "Tučný", "toolbar.button.email": "E-mail", diff --git a/kirby/i18n/translations/sv_SE.json b/kirby/i18n/translations/sv_SE.json index 1437052..a4565ef 100644 --- a/kirby/i18n/translations/sv_SE.json +++ b/kirby/i18n/translations/sv_SE.json @@ -49,6 +49,9 @@ "email": "E-postadress", "email.placeholder": "namn@exempel.se", + "entries": "Poster", + "entry": "Post", + "environment": "Miljö", "error.access.code": "Ogiltig kod", @@ -157,7 +160,7 @@ "error.template.default.notFound": "Standardmallen existerar inte", - "error.unexpected": "An unexpected error occurred! Enable debug mode for more info: https://getkirby.com/docs/reference/system/options/debug", + "error.unexpected": "Ett oväntat fel uppstod! Aktivera felsökningsläge för mer information: https://getkirby.com/docs/reference/system/options/debug", "error.user.changeEmail.permission": "Du har inte behörighet att ändra e-postadressen för användaren \"{name}\"", "error.user.changeLanguage.permission": "Du har inte behörighet att ändra språket för användaren \"{name}\"", @@ -294,6 +297,7 @@ "hide": "Göm", "hour": "Timme", "import": "Importera", + "info": "Info", "insert": "Infoga", "insert.after": "Infoga efter", "insert.before": "Infoga före", @@ -336,10 +340,12 @@ "license": "Licens", "license.buy": "Köp en licens", "license.register": "Registrera", + "license.manage": "Hantera dina licenser", "license.register.help": "Du fick din licenskod via e-post efter inköpet. Kopiera och klistra in den för att registrera licensen.", "license.register.label": "Ange din licenskod", "license.register.success": "Tack för att du stödjer Kirby", "license.unregistered": "Detta är en oregistrerad demo av Kirby", + "license.unregistered.label": "Oregistrerad", "link": "L\u00e4nk", "link.text": "L\u00e4nktext", @@ -469,20 +475,28 @@ "section.required": "Sektionen krävs", + "security": "Säkerhet", "select": "Välj", + "server": "Server", "settings": "Inställningar", "show": "Visa", + "site.blueprint": "Webbplatsen har ingen blueprint än. Du kan skapa en i /site/blueprints/site.yml", "size": "Storlek", "slug": "URL-appendix", "sort": "Sortera", + + "stats.empty": "Inga rapporter", + "system.issues.content": "Mappen content verkar vara exponerad", + "system.issues.debug": "Felsökningsläget måste vara avstängt i produktion", + "system.issues.git": "Mappen .git verkar vara exponerad", + "system.issues.https": "Vi rekommenderar HTTPS för alla dina webbplatser", + "system.issues.kirby": "Mappen kirby verkar vara exponerad", + "system.issues.site": "Mappen site verkar vara exponerad", + "title": "Titel", "template": "Mall", "today": "Idag", - "server": "Server", - - "site.blueprint": "Webbplatsen har ingen blueprint än. Du kan skapa en i /site/blueprints/site.yml", - "toolbar.button.code": "Kod", "toolbar.button.bold": "Fet", "toolbar.button.email": "E-post", diff --git a/kirby/i18n/translations/tr.json b/kirby/i18n/translations/tr.json index 43675e2..94e21dd 100644 --- a/kirby/i18n/translations/tr.json +++ b/kirby/i18n/translations/tr.json @@ -49,6 +49,9 @@ "email": "E-Posta", "email.placeholder": "eposta@ornek.com", + "entries": "Girdiler", + "entry": "Girdi", + "environment": "Ortam", "error.access.code": "Geçersiz kod", @@ -294,6 +297,7 @@ "hide": "Gizle", "hour": "Saat", "import": "İçe aktar", + "info": "Bilgi", "insert": "Ekle", "insert.after": "Sonrasına ekle", "insert.before": "Öncesine ekle", @@ -336,10 +340,12 @@ "license": "Lisans", "license.buy": "Bir lisans satın al", "license.register": "Kayıt Ol", + "license.manage": "Lisanslarınızı yönetin", "license.register.help": "Satın alma işleminden sonra e-posta yoluyla lisans kodunuzu aldınız. Lütfen kayıt olmak için kodu kopyalayıp yapıştırın.", "license.register.label": "Lütfen lisans kodunu giriniz", "license.register.success": "Kirby'yi desteklediğiniz için teşekkürler", "license.unregistered": "Bu Kirby'nin kayıtsız bir demosu", + "license.unregistered.label": "Kayıtsız", "link": "Ba\u011flant\u0131", "link.text": "Ba\u011flant\u0131 yaz\u0131s\u0131", @@ -354,7 +360,7 @@ "lock.unlock": "Kilidi Aç", "lock.isUnlocked": "Kaydedilmemiş değişikliklerin üzerine başka bir kullanıcı yazmış. Değişikliklerinizi el ile birleştirmek için değişikliklerinizi indirebilirsiniz.", - "login": "Giri\u015f", + "login": "Giriş", "login.code.label.login": "Giriş kodu", "login.code.label.password-reset": "Şifre sıfırlama kodu", "login.code.placeholder.email": "000 000", @@ -469,20 +475,28 @@ "section.required": "Bölüm gereklidir", + "security": "Güvenlik", "select": "Seç", + "server": "Sunucu", "settings": "Ayarlar", "show": "Göster", + "site.blueprint": "Sitenin henüz bir planı yok. Kurulumu /site/blueprints/site.yml'de tanımlayabilirsiniz.", "size": "Boyut", "slug": "Web Adres Uzantısı", "sort": "Sırala", + + "stats.empty": "Rapor yok", + "system.issues.content": "İçerik klasörü açığa çıkmış görünüyor", + "system.issues.debug": "Canlı modda hata ayıklama kapatılmalıdır", + "system.issues.git": ".git klasörü açığa çıkmış görünüyor", + "system.issues.https": "Tüm siteleriniz için HTTPS'yi öneriyoruz", + "system.issues.kirby": "Kirby klasörü açığa çıkmış görünüyor", + "system.issues.site": "Site klasörü açığa çıkmış görünüyor", + "title": "Başlık", "template": "\u015eablon", "today": "Bugün", - "server": "Sunucu", - - "site.blueprint": "Sitenin henüz bir planı yok. Kurulumu /site/blueprints/site.yml'de tanımlayabilirsiniz.", - "toolbar.button.code": "Kod", "toolbar.button.bold": "Kalın Yazı", "toolbar.button.email": "E-Posta", diff --git a/kirby/panel/.eslintrc.js b/kirby/panel/.eslintrc.js deleted file mode 100644 index 78d893b..0000000 --- a/kirby/panel/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - extends: [ - "eslint:recommended", - "plugin:cypress/recommended", - "plugin:vue/recommended", - "prettier" - ], - rules: { - "vue/attributes-order": "error", - "vue/component-definition-name-casing": "off", - "vue/html-closing-bracket-newline": [ - "error", - { - singleline: "never", - multiline: "always" - } - ], - "vue/multi-word-component-names": "off", - "vue/require-default-prop": "off", - "vue/require-prop-types": "error" - } -}; diff --git a/kirby/panel/.prettierrc.json b/kirby/panel/.prettierrc.json deleted file mode 100644 index 36b3563..0000000 --- a/kirby/panel/.prettierrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "trailingComma": "none" -} diff --git a/kirby/panel/cypress.config.js b/kirby/panel/cypress.config.js new file mode 100644 index 0000000..bbb1589 --- /dev/null +++ b/kirby/panel/cypress.config.js @@ -0,0 +1,11 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + video: false, + + e2e: { + baseUrl: "http://sandbox.test", + specPattern: "src/**/*.e2e.js", + supportFile: false + } +}); diff --git a/kirby/panel/dist/css/style.css b/kirby/panel/dist/css/style.css index d9fe2bb..a82b55c 100644 --- a/kirby/panel/dist/css/style.css +++ b/kirby/panel/dist/css/style.css @@ -1 +1 @@ -:root{--color-backdrop:rgba(0, 0, 0, .6);--color-black:#000;--color-dark:#313740;--color-light:var(--color-gray-200);--color-white:#fff;--color-gray-100:#f7f7f7;--color-gray-200:#efefef;--color-gray-300:#ddd;--color-gray-400:#ccc;--color-gray-500:#999;--color-gray-600:#777;--color-gray-700:#555;--color-gray-800:#333;--color-gray-900:#111;--color-gray:var(--color-gray-600);--color-red-200:#edc1c1;--color-red-300:#e3a0a0;--color-red-400:#d16464;--color-red-600:#c82829;--color-red:var(--color-red-600);--color-orange-200:#f2d4bf;--color-orange-300:#ebbe9e;--color-orange-400:#de935f;--color-orange-600:#f4861f;--color-orange:var(--color-orange-600);--color-yellow-200:#f9e8c7;--color-yellow-300:#f7e2b8;--color-yellow-400:#f0c674;--color-yellow-600:#cca000;--color-yellow:var(--color-yellow-600);--color-green-200:#dce5c2;--color-green-300:#c6d49d;--color-green-400:#a7bd68;--color-green-600:#5d800d;--color-green:var(--color-green-600);--color-aqua-200:#d0e5e2;--color-aqua-300:#bbd9d5;--color-aqua-400:#8abeb7;--color-aqua-600:#398e93;--color-aqua:var(--color-aqua-600);--color-blue-200:#cbd7e5;--color-blue-300:#b1c2d8;--color-blue-400:#7e9abf;--color-blue-600:#4271ae;--color-blue:var(--color-blue-600);--color-purple-200:#e0d4e4;--color-purple-300:#d4c3d9;--color-purple-400:#b294bb;--color-purple-600:#9c48b9;--color-purple:var(--color-purple-600);--container:80rem;--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-mono:"SFMono-Regular", Consolas, Liberation Mono, Menlo, Courier, monospace;--font-normal:400;--font-bold:600;--leading-none:1;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--rounded-xs:1px;--rounded-sm:.125rem;--rounded:.25rem;--shadow:0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06);--shadow-md:0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06);--shadow-lg:0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05);--shadow-xl:0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04);--shadow-outline:currentColor 0 0 0 2px;--shadow-inset:inset 0 2px 4px 0 rgba(0, 0, 0, .06);--spacing-0:0;--spacing-px:1px;--spacing-2px:2px;--spacing-1:.25rem;--spacing-2:.5rem;--spacing-3:.75rem;--spacing-4:1rem;--spacing-5:1.25rem;--spacing-6:1.5rem;--spacing-8:2rem;--spacing-10:2.5rem;--spacing-12:3rem;--spacing-16:4rem;--spacing-20:5rem;--spacing-24:6rem;--spacing-36:9rem;--text-xs:.75rem;--text-sm:.875rem;--text-base:1rem;--text-lg:1.125rem;--text-xl:1.25rem;--text-2xl:1.5rem;--text-3xl:1.75rem;--text-4xl:2.5rem;--text-5xl:3rem;--text-6xl:4rem;--color-background:var(--color-light);--color-border:var(--color-gray-400);--color-focus:var(--color-blue-600);--color-focus-light:var(--color-blue-400);--color-focus-outline:rgba(113, 143, 183, .25);--color-negative:var(--color-red-600);--color-negative-light:var(--color-red-400);--color-negative-outline:rgba(212, 110, 110, .25);--color-notice:var(--color-orange-600);--color-notice-light:var(--color-orange-400);--color-positive:var(--color-green-600);--color-positive-light:var(--color-green-400);--color-positive-outline:rgba(128, 149, 65, .25);--color-text:var(--color-gray-900);--color-text-light:var(--color-gray-600);--z-offline:1200;--z-fatal:1100;--z-loader:1000;--z-notification:900;--z-dialog:800;--z-navigation:700;--z-dropdown:600;--z-drawer:500;--z-dropzone:400;--z-toolbar:300;--z-content:200;--z-background:100;--bg-pattern:repeating-conic-gradient( rgba(0, 0, 0, 0) 0% 25%, rgba(0, 0, 0, .2) 0% 50% ) 50% / 20px 20px;--shadow-sticky:rgba(0, 0, 0, .05) 0 2px 5px;--shadow-dropdown:var(--shadow-lg);--shadow-item:var(--shadow);--field-input-padding:.5rem;--field-input-height:2.25rem;--field-input-line-height:1.25rem;--field-input-font-size:var(--text-base);--field-input-color-before:var(--color-gray-700);--field-input-color-after:var(--color-gray-700);--field-input-border:1px solid var(--color-border);--field-input-focus-border:1px solid var(--color-focus);--field-input-focus-outline:2px solid var(--color-focus-outline);--field-input-invalid-border:1px solid var(--color-negative-outline);--field-input-invalid-outline:0;--field-input-invalid-focus-border:1px solid var(--color-negative);--field-input-invalid-focus-outline:2px solid var(--color-negative-outline);--field-input-background:var(--color-white);--field-input-disabled-color:var(--color-gray-500);--field-input-disabled-background:var(--color-white);--field-input-disabled-border:1px solid var(--color-gray-300);--font-family-sans:var(--font-sans);--font-family-mono:var(--font-mono);--font-size-tiny:var(--text-xs);--font-size-small:var(--text-sm);--font-size-medium:var(--text-base);--font-size-large:var(--text-xl);--font-size-huge:var(--text-2xl);--font-size-monster:var(--text-3xl);--box-shadow-dropdown:var(--shadow-dropdown);--box-shadow-item:var(--shadow);--box-shadow-focus:var(--shadow-xl)}*,:after,:before{margin:0;padding:0;box-sizing:border-box}noscript{padding:1.5rem;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center}html{font-family:var(--font-sans);background:var(--color-background)}body,html{color:var(--color-gray-900);height:100%;overflow:hidden}a{color:inherit;text-decoration:none}li{list-style:none}b,strong{font-weight:var(--font-bold)}@keyframes LoadingCursor{to{cursor:progress}}@keyframes Spin{to{transform:rotate(360deg)}}.k-dialog{position:relative;background:var(--color-background);width:100%;box-shadow:var(--shadow-lg);border-radius:var(--rounded-xs);line-height:1;max-height:calc(100vh - 3rem);margin:1.5rem;display:flex;flex-direction:column}@media screen and (min-width:20rem){.k-dialog[data-size=small]{width:20rem}}@media screen and (min-width:22rem){.k-dialog[data-size=default]{width:22rem}}@media screen and (min-width:30rem){.k-dialog[data-size=medium]{width:30rem}}@media screen and (min-width:40rem){.k-dialog[data-size=large]{width:40rem}}.k-dialog-notification{padding:.75rem 1.5rem;background:var(--color-gray-900);width:100%;line-height:1.25rem;color:var(--color-white);display:flex;flex-shrink:0;align-items:center}.k-dialog-notification[data-theme]{background:var(--theme-light);color:var(--color-black)}.k-dialog-notification p{flex-grow:1;word-wrap:break-word;overflow:hidden}[dir=ltr] .k-dialog-notification .k-button{margin-left:1rem}[dir=rtl] .k-dialog-notification .k-button{margin-right:1rem}.k-dialog-notification .k-button{display:flex}.k-dialog-body{padding:1.5rem}.k-dialog-body .k-fieldset{padding-bottom:.5rem}[dir=ltr] .k-dialog-footer,[dir=rtl] .k-dialog-footer{border-bottom-right-radius:var(--rounded-xs);border-bottom-left-radius:var(--rounded-xs)}.k-dialog-footer{padding:0;border-top:1px solid var(--color-gray-300);line-height:1;flex-shrink:0}.k-dialog-footer .k-button-group{display:flex;margin:0;justify-content:space-between}.k-dialog-footer .k-button-group .k-button{padding:.75rem 1rem;line-height:1.25rem}[dir=ltr] .k-dialog-footer .k-button-group .k-button:first-child{text-align:left}[dir=rtl] .k-dialog-footer .k-button-group .k-button:first-child{text-align:right}[dir=ltr] .k-dialog-footer .k-button-group .k-button:first-child{padding-left:1.5rem}[dir=rtl] .k-dialog-footer .k-button-group .k-button:first-child{padding-right:1.5rem}[dir=ltr] .k-dialog-footer .k-button-group .k-button:last-child{text-align:right}[dir=rtl] .k-dialog-footer .k-button-group .k-button:last-child{text-align:left}[dir=ltr] .k-dialog-footer .k-button-group .k-button:last-child{padding-right:1.5rem}[dir=rtl] .k-dialog-footer .k-button-group .k-button:last-child{padding-left:1.5rem}.k-dialog-pagination{margin-bottom:-1.5rem;display:flex;justify-content:center;align-items:center}.k-dialog-search{margin-bottom:.75rem}.k-dialog-search.k-input{background:rgba(0,0,0,.075);padding:0 1rem;height:36px;border-radius:var(--rounded-xs)}.k-error-details{background:var(--color-white);display:block;overflow:auto;padding:1rem;font-size:var(--text-sm);line-height:1.25em;margin-top:.75rem}.k-error-details dt{color:var(--color-negative-light);margin-bottom:.25rem}.k-error-details dd{overflow:hidden;overflow-wrap:break-word;text-overflow:ellipsis}.k-error-details dd:not(:last-of-type){margin-bottom:1.5em}.k-error-details li:not(:last-child){border-bottom:1px solid var(--color-background);padding-bottom:.25rem;margin-bottom:.25rem}.k-files-dialog .k-list-item{cursor:pointer}[dir=ltr] .k-pages-dialog-navbar{padding-right:38px}[dir=rtl] .k-pages-dialog-navbar{padding-left:38px}.k-pages-dialog-navbar{display:flex;align-items:center;justify-content:center;margin-bottom:.5rem}.k-pages-dialog-navbar .k-button{width:38px}.k-pages-dialog-navbar .k-button[disabled]{opacity:0}.k-pages-dialog-navbar .k-headline{flex-grow:1;text-align:center}.k-pages-dialog .k-list-item{cursor:pointer}.k-pages-dialog .k-list-item .k-button[data-theme=disabled],.k-pages-dialog .k-list-item .k-button[disabled]{opacity:.25}.k-pages-dialog .k-list-item .k-button[data-theme=disabled]:hover{opacity:1}.k-users-dialog .k-list-item{cursor:pointer}.k-drawer{--drawer-header-height:2.5rem;--drawer-header-padding:1.5rem;position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--z-toolbar);display:flex;align-items:stretch;justify-content:flex-end;background:rgba(0,0,0,.2)}.k-drawer-box{position:relative;flex-basis:50rem;display:flex;flex-direction:column;background:var(--color-background);box-shadow:var(--shadow-xl)}[dir=ltr] .k-drawer-header{padding-left:var(--drawer-header-padding)}[dir=rtl] .k-drawer-header{padding-right:var(--drawer-header-padding)}.k-drawer-header{flex-shrink:0;height:var(--drawer-header-height);display:flex;align-items:center;line-height:1;justify-content:space-between;background:var(--color-white);font-size:var(--text-sm)}.k-drawer-title{padding:0 .75rem}[dir=ltr] .k-drawer-breadcrumb,[dir=ltr] .k-drawer-title{margin-left:-.75rem}[dir=rtl] .k-drawer-breadcrumb,[dir=rtl] .k-drawer-title{margin-right:-.75rem}.k-drawer-breadcrumb,.k-drawer-title{display:flex;flex-grow:1;align-items:center;min-width:0;font-size:var(--text-sm);font-weight:var(--font-normal)}[dir=ltr] .k-drawer-breadcrumb li:not(:last-child) .k-button:after{right:-.75rem}[dir=rtl] .k-drawer-breadcrumb li:not(:last-child) .k-button:after{left:-.75rem}.k-drawer-breadcrumb li:not(:last-child) .k-button:after{position:absolute;width:1.5rem;display:inline-flex;justify-content:center;align-items:center;content:"\203a";color:var(--color-gray-500);height:var(--drawer-header-height)}[dir=ltr] .k-drawer-breadcrumb .k-icon,[dir=ltr] .k-drawer-title .k-icon{margin-right:.5rem}[dir=rtl] .k-drawer-breadcrumb .k-icon,[dir=rtl] .k-drawer-title .k-icon{margin-left:.5rem}.k-drawer-breadcrumb .k-icon,.k-drawer-title .k-icon{width:1rem;color:var(--color-gray-500)}.k-drawer-breadcrumb .k-button{display:inline-flex;align-items:center;height:var(--drawer-header-height);padding-left:.75rem;padding-right:.75rem}.k-drawer-breadcrumb .k-button-text{opacity:1}[dir=ltr] .k-drawer-breadcrumb .k-button .k-button-icon~.k-button-text{padding-left:0}[dir=rtl] .k-drawer-breadcrumb .k-button .k-button-icon~.k-button-text{padding-right:0}[dir=ltr] .k-drawer-tabs{margin-right:.75rem}[dir=rtl] .k-drawer-tabs{margin-left:.75rem}.k-drawer-tabs{display:flex;align-items:center;line-height:1}.k-drawer-tab.k-button{height:var(--drawer-header-height);padding-left:.75rem;padding-right:.75rem;display:flex;align-items:center;font-size:var(--text-xs)}.k-drawer-tab.k-button[aria-current]:after{position:absolute;bottom:-1px;left:.75rem;right:.75rem;content:"";background:var(--color-black);height:2px}[dir=ltr] .k-drawer-options{padding-right:.75rem}[dir=rtl] .k-drawer-options{padding-left:.75rem}.k-drawer-option.k-button{width:var(--drawer-header-height);height:var(--drawer-header-height);color:var(--color-gray-500);line-height:1}.k-drawer-option.k-button:focus,.k-drawer-option.k-button:hover{color:var(--color-black)}.k-drawer-body{padding:1.5rem;flex-grow:1;background:var(--color-background)}.k-drawer[data-nested=true]{background:0 0}.k-calendar-input{--cell-padding:.25rem .5rem;padding:.5rem;background:var(--color-gray-900);color:var(--color-light);border-radius:var(--rounded-xs)}.k-calendar-table{table-layout:fixed;width:100%;min-width:15rem;padding-top:.5rem}.k-calendar-input>nav{display:flex;direction:ltr}.k-calendar-input>nav .k-button{padding:.5rem}.k-calendar-selects{flex-grow:1;display:flex;align-items:center;justify-content:center}[dir=ltr] .k-calendar-selects{direction:ltr}[dir=rtl] .k-calendar-selects{direction:rtl}.k-calendar-selects .k-select-input{padding:0 .5rem;font-weight:var(--font-normal);font-size:var(--text-sm)}.k-calendar-selects .k-select-input:focus-within{color:var(--color-focus-light)!important}.k-calendar-input th{padding:.5rem 0;color:var(--color-gray-500);font-size:var(--text-xs);font-weight:400;text-align:center}.k-calendar-day .k-button{width:2rem;height:2rem;margin-left:auto;margin-right:auto;color:var(--color-white);line-height:1.75rem;display:flex;justify-content:center;border-radius:50%;border:2px solid transparent}.k-calendar-day .k-button .k-button-text{opacity:1}.k-calendar-table .k-button:hover{color:var(--color-white)}.k-calendar-day:hover .k-button:not([data-disabled=true]){border-color:#ffffff40}.k-calendar-day[aria-current=date] .k-button{text-decoration:underline}.k-calendar-day[aria-selected=date] .k-button{border-color:currentColor;font-weight:600;color:var(--color-focus-light)}.k-calendar-today{text-align:center;padding-top:.5rem}.k-calendar-today .k-button{font-size:var(--text-xs);padding:1rem;text-decoration:underline}.k-calendar-today .k-button-text{opacity:1;vertical-align:baseline}.k-counter{font-size:var(--text-xs);color:var(--color-gray-900);font-weight:var(--font-bold)}.k-counter[data-invalid=true]{box-shadow:none;border:0;color:var(--color-negative)}[dir=ltr] .k-counter-rules{padding-left:.5rem}[dir=rtl] .k-counter-rules{padding-right:.5rem}.k-counter-rules{color:var(--color-gray-600);font-weight:var(--font-normal)}.k-form-submitter{display:none}.k-form-buttons[data-theme]{background:var(--theme-light)}.k-form-buttons .k-view{display:flex;justify-content:space-between;align-items:center}.k-form-button.k-button{font-weight:500;white-space:nowrap;line-height:1;height:2.5rem;display:flex;padding:0 1rem;align-items:center}[dir=ltr] .k-form-button:first-child{margin-left:-1rem}[dir=rtl] .k-form-button:first-child{margin-right:-1rem}[dir=ltr] .k-form-button:last-child{margin-right:-1rem}[dir=rtl] .k-form-button:last-child{margin-left:-1rem}[dir=ltr] .k-form-lock-info{margin-right:3rem}[dir=rtl] .k-form-lock-info{margin-left:3rem}.k-form-lock-info{display:flex;font-size:var(--text-sm);align-items:center;line-height:1.5em;padding:.625rem 0}[dir=ltr] .k-form-lock-info>.k-icon{margin-right:.5rem}[dir=rtl] .k-form-lock-info>.k-icon{margin-left:.5rem}.k-form-lock-buttons{display:flex;flex-shrink:0}.k-form-lock-loader{animation:Spin 4s linear infinite}.k-form-lock-loader .k-icon-loader{display:flex}.k-form-indicator-toggle{color:var(--color-notice-light)}.k-form-indicator-info{font-size:var(--text-sm);font-weight:var(--font-bold);padding:.75rem 1rem .25rem;line-height:1.25em;width:15rem}.k-field-label{font-weight:var(--font-bold);display:block;padding:0 0 .75rem;flex-grow:1;line-height:1.25rem}[dir=ltr] .k-field-label abbr{padding-left:.25rem}[dir=rtl] .k-field-label abbr{padding-right:.25rem}.k-field-label abbr{text-decoration:none;color:var(--color-gray-500)}.k-field-header{position:relative;display:flex;align-items:baseline}[dir=ltr] .k-field-options{right:0}[dir=rtl] .k-field-options{left:0}.k-field-options{position:absolute;top:calc(-.5rem - 1px)}.k-field-options.k-button-group .k-dropdown{height:auto}.k-field-options.k-button-group .k-field-options-button.k-button{padding:.75rem;display:flex}.k-field[data-disabled=true]{cursor:not-allowed}.k-field[data-disabled=true] *{pointer-events:none}.k-field[data-disabled=true] .k-text[data-theme=help] *{pointer-events:initial}.k-field-counter{display:none}.k-field:focus-within>.k-field-header>.k-field-counter{display:block}.k-field-help{padding-top:.5rem}.k-fieldset{border:0}.k-fieldset .k-grid{grid-row-gap:2.25rem}@media screen and (min-width:30em){.k-fieldset .k-grid{grid-column-gap:1.5rem}}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid{grid-template-columns:repeat(1,1fr)}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid .k-column,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid .k-column{grid-column-start:initial}.k-input{display:flex;align-items:center;line-height:1;border:0;outline:0;background:0 0}.k-input-element{flex-grow:1}.k-input-icon{display:flex;justify-content:center;align-items:center;line-height:0}.k-input[data-disabled=true]{pointer-events:none}[data-disabled=true] .k-input-icon{color:var(--color-gray-600)}.k-input[data-theme=field]{line-height:1;border:var(--field-input-border);background:var(--field-input-background)}.k-input[data-theme=field]:focus-within{border:var(--field-input-focus-border);box-shadow:var(--color-focus-outline) 0 0 0 2px}.k-input[data-theme=field][data-disabled=true]{background:var(--color-background)}.k-input[data-theme=field] .k-input-icon{width:var(--field-input-height);align-self:stretch;display:flex;align-items:center;flex-shrink:0}.k-input[data-theme=field] .k-input-after,.k-input[data-theme=field] .k-input-before{align-self:stretch;display:flex;align-items:center;flex-shrink:0;padding:0 var(--field-input-padding)}[dir=ltr] .k-input[data-theme=field] .k-input-before{padding-right:0}[dir=ltr] .k-input[data-theme=field] .k-input-after,[dir=rtl] .k-input[data-theme=field] .k-input-before{padding-left:0}.k-input[data-theme=field] .k-input-before{color:var(--field-input-color-before)}[dir=rtl] .k-input[data-theme=field] .k-input-after{padding-right:0}.k-input[data-theme=field] .k-input-after{color:var(--field-input-color-after)}.k-input[data-theme=field] .k-input-icon>.k-dropdown{width:100%;height:100%}.k-input[data-theme=field] .k-input-icon-button{width:100%;height:100%;display:flex;align-items:center;justify-content:center;flex-shrink:0}.k-input[data-theme=field] .k-number-input,.k-input[data-theme=field] .k-select-input,.k-input[data-theme=field] .k-text-input{padding:var(--field-input-padding);line-height:var(--field-input-line-height)}.k-input[data-theme=field] .k-date-input .k-select-input,.k-input[data-theme=field] .k-time-input .k-select-input{padding-left:0;padding-right:0}[dir=ltr] .k-input[data-theme=field] .k-date-input .k-select-input:first-child,[dir=ltr] .k-input[data-theme=field] .k-time-input .k-select-input:first-child{padding-left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field] .k-date-input .k-select-input:first-child,[dir=rtl] .k-input[data-theme=field] .k-time-input .k-select-input:first-child{padding-right:var(--field-input-padding)}.k-input[data-theme=field] .k-date-input .k-select-input:focus-within,.k-input[data-theme=field] .k-time-input .k-select-input:focus-within{color:var(--color-focus);font-weight:var(--font-bold)}[dir=ltr] .k-input[data-theme=field].k-time-input .k-time-input-meridiem{padding-left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field].k-time-input .k-time-input-meridiem{padding-right:var(--field-input-padding)}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,.k-input[data-theme=field][data-type=checkboxes] .k-radio-input li,.k-input[data-theme=field][data-type=radio] .k-checkboxes-input li,.k-input[data-theme=field][data-type=radio] .k-radio-input li{min-width:0;overflow-wrap:break-word}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-input-before{border-right:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-after,[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-icon,[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-input-before{border-left:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-after,[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-icon{border-right:1px solid var(--color-background)}.k-input[data-theme=field][data-type=checkboxes] .k-input-element{overflow:hidden}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{margin-right:-1px}[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{margin-left:-1px}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px}@media screen and (min-width:65em){.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{grid-template-columns:repeat(var(--columns),1fr)}}[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li{border-left:1px solid var(--color-background)}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,.k-input[data-theme=field][data-type=radio] .k-radio-input li{border-bottom:1px solid var(--color-background)}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input label{display:block;line-height:var(--field-input-line-height);padding:var(--field-input-padding) var(--field-input-padding)}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon,[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-radio-input label:before{left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-radio-input label:before{right:var(--field-input-padding)}.k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon{top:calc((var(--field-input-height) - var(--field-input-font-size))/2);margin-top:0}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-input-before{border-right:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-after,[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-icon,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-input-before{border-left:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-radio-input li,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-after,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-icon{border-right:1px solid var(--color-background)}.k-input[data-theme=field][data-type=radio] .k-input-element{overflow:hidden}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-radio-input{margin-right:-1px}[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-radio-input{margin-left:-1px}.k-input[data-theme=field][data-type=radio] .k-radio-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px}@media screen and (min-width:65em){.k-input[data-theme=field][data-type=radio] .k-radio-input{grid-template-columns:repeat(var(--columns),1fr)}}[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-radio-input li{border-left:1px solid var(--color-background)}.k-input[data-theme=field][data-type=radio] .k-radio-input label{display:block;flex-grow:1;min-height:var(--field-input-height);line-height:var(--field-input-line-height);padding:calc((var(--field-input-height) - var(--field-input-line-height))/2) var(--field-input-padding)}.k-input[data-theme=field][data-type=radio] .k-radio-input label:before{top:calc((var(--field-input-height) - 1rem)/2);margin-top:-1px}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-radio-input-info{display:block;font-size:var(--text-sm);color:var(--color-gray-600);line-height:var(--field-input-line-height);padding-top:calc(var(--field-input-line-height)/10)}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-icon{width:var(--field-input-height);height:var(--field-input-height);display:flex;align-items:center;justify-content:center}.k-input[data-theme=field][data-type=range] .k-range-input{padding:var(--field-input-padding)}.k-input[data-theme=field][data-type=multiselect],.k-input[data-theme=field][data-type=select]{position:relative}[dir=ltr] .k-input[data-theme=field][data-type=select] .k-input-icon{right:0}[dir=rtl] .k-input[data-theme=field][data-type=select] .k-input-icon{left:0}.k-input[data-theme=field][data-type=select] .k-input-icon{position:absolute;top:0;bottom:0}.k-input[data-theme=field][data-type=tags] .k-tags-input{padding:.25rem .25rem 0}[dir=ltr] .k-input[data-theme=field][data-type=tags] .k-tag{margin-right:.25rem}[dir=rtl] .k-input[data-theme=field][data-type=tags] .k-tag{margin-left:.25rem}.k-input[data-theme=field][data-type=tags] .k-tag{margin-bottom:.25rem;height:auto;min-height:1.75rem;font-size:var(--text-sm)}.k-input[data-theme=field][data-type=tags] .k-tags-input input{font-size:var(--text-sm);padding:0 .25rem;height:1.75rem;line-height:1;margin-bottom:.25rem}.k-input[data-theme=field][data-type=tags] .k-tags-input .k-dropdown-content{top:calc(100% + .5rem + 2px)}.k-input[data-theme=field][data-type=tags] .k-tags-input .k-dropdown-content[data-dropup]{top:calc(100% + .5rem + 2px);bottom:initial;margin-bottom:initial}.k-input[data-theme=field][data-type=multiselect] .k-multiselect-input{padding:.25rem 2rem 0 .25rem;min-height:2.25rem}[dir=ltr] .k-input[data-theme=field][data-type=multiselect] .k-tag{margin-right:.25rem}[dir=rtl] .k-input[data-theme=field][data-type=multiselect] .k-tag{margin-left:.25rem}.k-input[data-theme=field][data-type=multiselect] .k-tag{margin-bottom:.25rem;height:1.75rem;font-size:var(--text-sm)}[dir=ltr] .k-input[data-theme=field][data-type=multiselect] .k-input-icon{right:0}[dir=rtl] .k-input[data-theme=field][data-type=multiselect] .k-input-icon{left:0}.k-input[data-theme=field][data-type=multiselect] .k-input-icon{position:absolute;top:0;bottom:0;pointer-events:none}.k-input[data-theme=field][data-type=textarea] .k-textarea-input-native{padding:.25rem var(--field-input-padding);line-height:1.5rem}[dir=ltr] .k-input[data-theme=field][data-type=toggle] .k-input-before{padding-right:calc(var(--field-input-padding)/2)}[dir=rtl] .k-input[data-theme=field][data-type=toggle] .k-input-before{padding-left:calc(var(--field-input-padding)/2)}[dir=ltr] .k-input[data-theme=field][data-type=toggle] .k-toggle-input{padding-left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field][data-type=toggle] .k-toggle-input{padding-right:var(--field-input-padding)}.k-input[data-theme=field][data-type=toggle] .k-toggle-input-label{padding:0 var(--field-input-padding)0 .75rem;line-height:var(--field-input-height)}.k-login-code-form .k-user-info{height:38px;margin-bottom:2.25rem;padding:.5rem;background:var(--color-white);border-radius:var(--rounded-xs);box-shadow:var(--shadow)}.k-times{padding:var(--spacing-4) var(--spacing-6);display:grid;line-height:1;grid-template-columns:1fr 1fr;grid-gap:var(--spacing-6)}.k-times .k-icon{width:1rem;margin-bottom:var(--spacing-2)}.k-times-slot .k-button{padding:var(--spacing-1) var(--spacing-3) var(--spacing-1)0;font-variant-numeric:tabular-nums;white-space:nowrap}.k-times .k-times-slot hr{position:relative;opacity:1;margin:var(--spacing-2)0;border:0;height:1px;top:1px;background:var(--color-dark)}[dir=ltr] .k-upload input{left:-3000px}[dir=rtl] .k-upload input{right:-3000px}.k-upload input{position:absolute;top:0}.k-upload-dialog .k-headline{margin-bottom:.75rem}.k-upload-error-list,.k-upload-list{line-height:1.5em;font-size:var(--text-sm)}.k-upload-list-filename{color:var(--color-gray-600)}.k-upload-error-list li{padding:.75rem;background:var(--color-white);border-radius:var(--rounded-xs)}.k-upload-error-list li:not(:last-child){margin-bottom:2px}.k-upload-error-filename{color:var(--color-negative);font-weight:var(--font-bold)}.k-upload-error-message{color:var(--color-gray-600)}.k-writer-toolbar{position:absolute;display:flex;background:var(--color-black);height:30px;transform:translate(-50%) translateY(-.75rem);z-index:calc(var(--z-dropdown) + 1);box-shadow:var(--shadow);color:var(--color-white);border-radius:var(--rounded)}.k-writer-toolbar-button.k-button{display:flex;align-items:center;justify-content:center;height:30px;width:30px;font-size:var(--text-sm)!important;color:currentColor;line-height:1}.k-writer-toolbar-button.k-button:hover{background:rgba(255,255,255,.15)}.k-writer-toolbar-button.k-writer-toolbar-button-active{color:var(--color-blue-300)}.k-writer-toolbar-button.k-writer-toolbar-nodes{width:auto;padding:0 .75rem}[dir=ltr] .k-writer-toolbar .k-dropdown+.k-writer-toolbar-button{border-left:1px solid var(--color-gray-700)}[dir=rtl] .k-writer-toolbar .k-dropdown+.k-writer-toolbar-button{border-right:1px solid var(--color-gray-700)}[dir=ltr] .k-writer-toolbar-button.k-writer-toolbar-nodes:after{margin-left:.5rem}[dir=rtl] .k-writer-toolbar-button.k-writer-toolbar-nodes:after{margin-right:.5rem}.k-writer-toolbar-button.k-writer-toolbar-nodes:after{content:"";border-top:4px solid var(--color-white);border-left:4px solid transparent;border-right:4px solid transparent}.k-writer-toolbar .k-dropdown-content{color:var(--color-black);background:var(--color-white);margin-top:.5rem}.k-writer-toolbar .k-dropdown-content .k-dropdown-item[aria-current]{color:var(--color-focus);font-weight:500}.k-writer{position:relative;width:100%;grid-template-areas:"content";display:grid}.k-writer .ProseMirror{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;white-space:pre-wrap;font-variant-ligatures:none;line-height:inherit;grid-area:content}.k-writer .ProseMirror:focus{outline:0}.k-writer .ProseMirror *{caret-color:currentColor}.k-writer .ProseMirror a{color:var(--color-focus);text-decoration:underline}.k-writer .ProseMirror>:last-child{margin-bottom:0}.k-writer .ProseMirror h1,.k-writer .ProseMirror h2,.k-writer .ProseMirror h3,.k-writer .ProseMirror ol,.k-writer .ProseMirror p,.k-writer .ProseMirror ul{margin-bottom:.75rem}.k-writer .ProseMirror h1{font-size:var(--text-3xl);line-height:1.25em}.k-writer .ProseMirror h2{font-size:var(--text-2xl);line-height:1.25em}.k-writer .ProseMirror h3{font-size:var(--text-xl);line-height:1.25em}.k-writer .ProseMirror h1 strong,.k-writer .ProseMirror h2 strong,.k-writer .ProseMirror h3 strong{font-weight:700}.k-writer .ProseMirror strong{font-weight:600}.k-writer .ProseMirror code{position:relative;font-size:.925em;display:inline-block;line-height:1.325;padding:.05em .325em;background:var(--color-gray-300);border-radius:var(--rounded)}[dir=ltr] .k-writer .ProseMirror ol,[dir=ltr] .k-writer .ProseMirror ul{padding-left:1rem}[dir=rtl] .k-writer .ProseMirror ol,[dir=rtl] .k-writer .ProseMirror ul{padding-right:1rem}.k-writer .ProseMirror ul>li{list-style:disc}.k-writer .ProseMirror ul ul>li{list-style:circle}.k-writer .ProseMirror ul ul ul>li{list-style:square}.k-writer .ProseMirror ol>li{list-style:decimal}.k-writer .ProseMirror li>ol,.k-writer .ProseMirror li>p,.k-writer .ProseMirror li>ul{margin:0}.k-writer-code pre{-moz-tab-size:2;-o-tab-size:2;tab-size:2;font-size:var(--text-sm);line-height:2em;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;white-space:pre}.k-writer .ProseMirror code,.k-writer-code code{font-family:var(--font-mono)}.k-writer[data-placeholder][data-empty=true]:before{grid-area:content;content:attr(data-placeholder);line-height:inherit;color:var(--color-gray-500);pointer-events:none;white-space:pre-wrap;word-wrap:break-word}.k-login-alert{padding:.5rem .75rem;display:flex;justify-content:space-between;align-items:center;min-height:38px;margin-bottom:2rem;background:var(--color-negative);color:var(--color-white);font-size:var(--text-sm);border-radius:var(--rounded-xs);box-shadow:var(--shadow-lg);cursor:pointer}.k-checkbox-input{position:relative;cursor:pointer}.k-checkbox-input-native{position:absolute;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:0;height:0;opacity:0}[dir=ltr] .k-checkbox-input-label{padding-left:1.75rem}[dir=rtl] .k-checkbox-input-label{padding-right:1.75rem}.k-checkbox-input-label{display:block}[dir=ltr] .k-checkbox-input-icon{left:0}[dir=rtl] .k-checkbox-input-icon{right:0}.k-checkbox-input-icon{position:absolute;width:1rem;height:1rem;border:2px solid var(--color-gray-500)}.k-checkbox-input-icon svg{position:absolute;width:12px;height:12px;display:none}.k-checkbox-input-icon path{stroke:var(--color-white)}.k-checkbox-input-native:checked+.k-checkbox-input-icon{border-color:var(--color-gray-900);background:var(--color-gray-900)}[data-disabled=true] .k-checkbox-input-native:checked+.k-checkbox-input-icon{border-color:var(--color-gray-600);background:var(--color-gray-600)}.k-checkbox-input-native:checked+.k-checkbox-input-icon svg{display:block}.k-checkbox-input-native:focus+.k-checkbox-input-icon{border-color:var(--color-blue-600)}.k-checkbox-input-native:focus:checked+.k-checkbox-input-icon{background:var(--color-focus)}.k-text-input{width:100%;border:0;background:0 0;font:inherit;color:inherit;font-variant-numeric:tabular-nums}.k-text-input::-moz-placeholder{color:var(--color-gray-500)}.k-text-input::placeholder{color:var(--color-gray-500)}.k-text-input:focus{outline:0}.k-text-input:invalid{box-shadow:none;outline:0}.k-list-input .ProseMirror{line-height:1.5em}.k-list-input .ProseMirror ol>li::marker{font-size:var(--text-sm);color:var(--color-gray-500)}.k-multiselect-input{display:flex;flex-wrap:wrap;position:relative;font-size:var(--text-sm);min-height:2.25rem;line-height:1}.k-multiselect-input .k-sortable-ghost{background:var(--color-focus)}.k-multiselect-input .k-dropdown-content,.k-multiselect-input[data-layout=list] .k-tag{width:100%}.k-multiselect-search{margin-top:0!important;color:var(--color-white);background:var(--color-gray-900);border-bottom:1px dashed rgba(255,255,255,.2)}.k-multiselect-search>.k-button-text{flex:1;opacity:1!important}.k-multiselect-search input{width:100%;color:var(--color-white);background:0 0;border:0;outline:0;padding:.25rem 0;font:inherit}.k-multiselect-options{position:relative;max-height:275px;padding:.5rem 0}.k-multiselect-option{position:relative}.k-multiselect-option.selected{color:var(--color-positive-light)}.k-multiselect-option.disabled:not(.selected) .k-icon{opacity:0}.k-multiselect-option b{color:var(--color-focus-light);font-weight:700}[dir=ltr] .k-multiselect-value{margin-left:.25rem}[dir=rtl] .k-multiselect-value{margin-right:.25rem}.k-multiselect-value{color:var(--color-gray-500)}.k-multiselect-value:before{content:" ("}.k-multiselect-value:after{content:")"}[dir=ltr] .k-multiselect-input[data-layout=list] .k-tag{margin-right:0!important}[dir=rtl] .k-multiselect-input[data-layout=list] .k-tag{margin-left:0!important}.k-multiselect-more{width:100%;padding:.75rem;color:#fffc;text-align:center;border-top:1px dashed rgba(255,255,255,.2)}.k-multiselect-more:hover{color:var(--color-white)}.k-number-input{width:100%;border:0;background:0 0;font:inherit;color:inherit}.k-number-input::-moz-placeholder{color:var(--color-gray-500)}.k-number-input::placeholder{color:var(--color-gray-500)}.k-number-input:focus{outline:0}.k-number-input:invalid{box-shadow:none;outline:0}[dir=ltr] .k-radio-input li{padding-left:1.75rem}[dir=rtl] .k-radio-input li{padding-right:1.75rem}.k-radio-input li{position:relative;line-height:1.5rem}.k-radio-input input{position:absolute;width:0;height:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;opacity:0}.k-radio-input label{cursor:pointer;align-items:center}[dir=ltr] .k-radio-input label:before{left:0}[dir=rtl] .k-radio-input label:before{right:0}.k-radio-input label:before{position:absolute;top:.175em;content:"";width:1rem;height:1rem;border-radius:50%;border:2px solid var(--color-gray-500);box-shadow:var(--color-white) 0 0 0 2px inset}.k-radio-input input:checked+label:before{border-color:var(--color-gray-900);background:var(--color-gray-900)}[data-disabled=true] .k-radio-input input:checked+label:before{border-color:var(--color-gray-600);background:var(--color-gray-600)}.k-radio-input input:focus+label:before{border-color:var(--color-blue-600)}.k-radio-input input:focus:checked+label:before{background:var(--color-focus)}.k-radio-input-text{display:block}.k-range-input{--range-thumb-size:16px;--range-thumb-border:4px solid var(--color-gray-900);--range-thumb-border-disabled:4px solid var(--color-gray-600);--range-thumb-background:var(--color-background);--range-thumb-focus-border:4px solid var(--color-focus);--range-thumb-focus-background:var(--color-background);--range-track-height:4px;--range-track-background:var(--color-border);--range-track-color:var(--color-gray-900);--range-track-color-disabled:var(--color-gray-600);--range-track-focus-color:var(--color-focus);display:flex;align-items:center}.k-range-input-native{--min:0;--max:100;--value:0;--range:calc(var(--max) - var(--min));--ratio:calc((var(--value) - var(--min)) / var(--range));--position:calc( .5 * var(--range-thumb-size) + var(--ratio) * calc(100% - var(--range-thumb-size)) );-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:var(--range-thumb-size);background:0 0;font-size:var(--text-sm);line-height:1}.k-range-input-native::-webkit-slider-thumb{-webkit-appearance:none;appearance:none}.k-range-input-native::-webkit-slider-runnable-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background)}.k-range-input-native::-moz-range-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background)}.k-range-input-native::-ms-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background)}.k-range-input-native::-webkit-slider-runnable-track{background:linear-gradient(var(--range-track-color),var(--range-track-color))0/var(--position) 100%no-repeat var(--range-track-background)}.k-range-input-native::-moz-range-progress{height:var(--range-track-height);background:var(--range-track-color)}.k-range-input-native::-ms-fill-lower{height:var(--range-track-height);background:var(--range-track-color)}.k-range-input-native::-webkit-slider-thumb{margin-top:calc(.5*(var(--range-track-height) - var(--range-thumb-size)));box-sizing:border-box;width:var(--range-thumb-size);height:var(--range-thumb-size);background:var(--range-thumb-background);border:var(--range-thumb-border);border-radius:50%;cursor:pointer}.k-range-input-native::-moz-range-thumb{box-sizing:border-box;width:var(--range-thumb-size);height:var(--range-thumb-size);background:var(--range-thumb-background);border:var(--range-thumb-border);border-radius:50%;cursor:pointer}.k-range-input-native::-ms-thumb{box-sizing:border-box;width:var(--range-thumb-size);height:var(--range-thumb-size);background:var(--range-thumb-background);border:var(--range-thumb-border);border-radius:50%;cursor:pointer;margin-top:0}.k-range-input-native::-ms-tooltip{display:none}.k-range-input-native:focus{outline:0}.k-range-input-native:focus::-webkit-slider-runnable-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background);background:linear-gradient(var(--range-track-focus-color),var(--range-track-focus-color))0/var(--position) 100%no-repeat var(--range-track-background)}.k-range-input-native:focus::-moz-range-progress{height:var(--range-track-height);background:var(--range-track-focus-color)}.k-range-input-native:focus::-ms-fill-lower{height:var(--range-track-height);background:var(--range-track-focus-color)}.k-range-input-native:focus::-webkit-slider-thumb{background:var(--range-thumb-focus-background);border:var(--range-thumb-focus-border)}.k-range-input-native:focus::-moz-range-thumb{background:var(--range-thumb-focus-background);border:var(--range-thumb-focus-border)}.k-range-input-native:focus::-ms-thumb{background:var(--range-thumb-focus-background);border:var(--range-thumb-focus-border)}[dir=ltr] .k-range-input-tooltip{margin-left:1rem}[dir=rtl] .k-range-input-tooltip{margin-right:1rem}.k-range-input-tooltip{position:relative;max-width:20%;display:flex;align-items:center;color:var(--color-white);font-size:var(--text-xs);line-height:1;text-align:center;border-radius:var(--rounded-xs);background:var(--color-gray-900);padding:0 .25rem;white-space:nowrap}[dir=ltr] .k-range-input-tooltip:after{left:-5px}[dir=rtl] .k-range-input-tooltip:after{right:-5px}[dir=ltr] .k-range-input-tooltip:after{border-right:5px solid var(--color-gray-900)}[dir=rtl] .k-range-input-tooltip:after{border-left:5px solid var(--color-gray-900)}.k-range-input-tooltip:after{position:absolute;top:50%;width:0;height:0;transform:translateY(-50%);border-top:5px solid transparent;border-bottom:5px solid transparent;content:""}.k-range-input-tooltip>*{padding:4px}[data-disabled=true] .k-range-input-native::-webkit-slider-runnable-track{background:linear-gradient(var(--range-track-color-disabled),var(--range-track-color-disabled))0/var(--position) 100%no-repeat var(--range-track-background)}[data-disabled=true] .k-range-input-native::-moz-range-progress{height:var(--range-track-height);background:var(--range-track-color-disabled)}[data-disabled=true] .k-range-input-native::-ms-fill-lower{height:var(--range-track-height);background:var(--range-track-color-disabled)}[data-disabled=true] .k-range-input-native::-webkit-slider-thumb{border:var(--range-thumb-border-disabled)}[data-disabled=true] .k-range-input-native::-moz-range-thumb{border:var(--range-thumb-border-disabled)}[data-disabled=true] .k-range-input-native::-ms-thumb{border:var(--range-thumb-border-disabled)}[data-disabled=true] .k-range-input-tooltip{background:var(--color-gray-600)}[dir=ltr] [data-disabled=true] .k-range-input-tooltip:after{border-right:5px solid var(--color-gray-600)}[dir=rtl] [data-disabled=true] .k-range-input-tooltip:after{border-left:5px solid var(--color-gray-600)}.k-select-input{position:relative;display:block;cursor:pointer;overflow:hidden}.k-select-input-native{position:absolute;top:0;right:0;bottom:0;left:0;opacity:0;width:100%;font:inherit;z-index:1;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-weight:var(--font-normal)}.k-select-input-native[disabled]{cursor:default}.k-tags-input{display:flex;flex-wrap:wrap}.k-tags-input .k-sortable-ghost{background:var(--color-focus)}.k-tags-input-element{flex-grow:1;flex-basis:0;min-width:0}.k-tags-input:focus-within .k-tags-input-element{flex-basis:4rem}.k-tags-input-element input{font:inherit;border:0;width:100%;background:0 0}.k-tags-input-element input:focus{outline:0}[dir=ltr] .k-tags-input[data-layout=list] .k-tag{margin-right:0!important}[dir=rtl] .k-tags-input[data-layout=list] .k-tag{margin-left:0!important}.k-tags-input[data-layout=list] .k-tag{width:100%}.k-textarea-input[data-size=small]{--size:7.5rem}.k-textarea-input[data-size=medium]{--size:15rem}.k-textarea-input[data-size=large]{--size:30rem}.k-textarea-input[data-size=huge]{--size:45rem}.k-textarea-input-wrapper{position:relative}.k-textarea-input-native{resize:none;border:0;width:100%;background:0 0;font:inherit;line-height:1.5em;color:inherit;min-height:var(--size)}.k-textarea-input-native::-moz-placeholder{color:var(--color-gray-500)}.k-textarea-input-native::placeholder{color:var(--color-gray-500)}.k-textarea-input-native:focus{outline:0}.k-textarea-input-native:invalid{box-shadow:none;outline:0}.k-textarea-input-native[data-font=monospace]{font-family:var(--font-mono)}.k-toolbar{margin-bottom:.25rem;color:#aaa}.k-textarea-input:focus-within .k-toolbar{position:sticky;top:0;left:0;right:0;z-index:1;box-shadow:#0000000d 0 2px 5px;border-bottom:1px solid rgba(0,0,0,.1);color:#000}.k-toggle-input{--toggle-background:var(--color-white);--toggle-color:var(--color-gray-500);--toggle-active-color:var(--color-gray-900);--toggle-focus-color:var(--color-focus);--toggle-height:16px;display:flex;align-items:center}.k-toggle-input-native{position:relative;height:var(--toggle-height);width:calc(var(--toggle-height)*2);border-radius:var(--toggle-height);border:2px solid var(--toggle-color);box-shadow:inset 0 0 0 2px var(--toggle-background),inset calc(var(--toggle-height)*-1) 0 0 2px var(--toggle-background);background-color:var(--toggle-color);outline:0;transition:all ease-in-out .1s;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;flex-shrink:0}.k-toggle-input-native:checked{border-color:var(--toggle-active-color);box-shadow:inset 0 0 0 2px var(--toggle-background),inset var(--toggle-height) 0 0 2px var(--toggle-background);background-color:var(--toggle-active-color)}.k-toggle-input-native[disabled]{border-color:var(--color-border);box-shadow:inset 0 0 0 2px var(--color-background),inset calc(var(--toggle-height)*-1) 0 0 2px var(--color-background);background-color:var(--color-border)}.k-toggle-input-native[disabled]:checked{box-shadow:inset 0 0 0 2px var(--color-background),inset var(--toggle-height) 0 0 2px var(--color-background)}.k-toggle-input-native:focus:checked{border:2px solid var(--color-focus);background-color:var(--toggle-focus-color)}.k-toggle-input-native::-ms-check{opacity:0}.k-toggle-input-label{cursor:pointer;flex-grow:1}.k-blocks-field{position:relative}.k-date-field-body{display:flex;flex-wrap:wrap;line-height:1;border:var(--field-input-border);background:var(--color-gray-300);gap:1px;--multiplier:calc(25rem - 100%)}.k-date-field-body:focus-within{border:var(--field-input-focus-border);box-shadow:var(--color-focus-outline) 0 0 0 2px}.k-date-field[data-disabled] .k-date-field-body{background:0 0}.k-date-field-body>.k-input[data-theme=field]{border:0;box-shadow:none;border-radius:var(--rounded-sm)}.k-date-field-body>.k-input[data-invalid=true],.k-date-field-body>.k-input[data-invalid=true]:focus-within{border:0!important;box-shadow:none!important}.k-date-field-body>*{flex-grow:1;flex-basis:calc(var(--multiplier)*999);max-width:100%}.k-date-field-body .k-input[data-type=date]{min-width:60%}.k-date-field-body .k-input[data-type=time]{min-width:30%}.k-files-field[data-disabled=true] *{pointer-events:all!important}body{counter-reset:headline-counter}.k-headline-field{position:relative;padding-top:1.5rem}.k-fieldset>.k-grid .k-column:first-child .k-headline-field{padding-top:0}[dir=ltr] .k-headline-field .k-headline[data-numbered]:before{padding-right:.25rem}[dir=rtl] .k-headline-field .k-headline[data-numbered]:before{padding-left:.25rem}.k-headline-field .k-headline[data-numbered]:before{counter-increment:headline-counter;content:counter(headline-counter,decimal-leading-zero);color:var(--color-focus);font-weight:400}.k-info-field .k-headline{padding-bottom:.75rem;line-height:1.25rem}.k-layout-column{position:relative;height:100%;display:flex;flex-direction:column;background:var(--color-white);min-height:6rem}.k-layout-column:focus{outline:0}.k-layout-column .k-blocks{background:0 0;box-shadow:none;padding:0;height:100%;background:var(--color-white);min-height:4rem}.k-layout-column .k-blocks[data-empty=true]{min-height:6rem}.k-layout-column .k-blocks-list{display:flex;flex-direction:column;height:100%}.k-layout-column .k-blocks .k-block-container:last-of-type{flex-grow:1}.k-layout-column .k-blocks-empty{position:absolute;top:0;right:0;bottom:0;left:0;justify-content:center;opacity:0;transition:opacity .3s;border:0}.k-layout-column .k-blocks-empty:hover{opacity:1}[dir=ltr] .k-layout-column .k-blocks-empty.k-empty .k-icon{border-right:0}[dir=rtl] .k-layout-column .k-blocks-empty.k-empty .k-icon{border-left:0}.k-layout-column .k-blocks-empty.k-empty .k-icon{width:1rem}[dir=ltr] .k-layout{padding-right:var(--layout-toolbar-width)}[dir=rtl] .k-layout{padding-left:var(--layout-toolbar-width)}.k-layout{--layout-border-color:var(--color-gray-300);--layout-toolbar-width:2rem;position:relative;background:#fff;box-shadow:var(--shadow)}[dir=ltr] [data-disabled=true] .k-layout{padding-right:0}[dir=rtl] [data-disabled=true] .k-layout{padding-left:0}.k-layout:not(:last-of-type){margin-bottom:1px}.k-layout:focus{outline:0}[dir=ltr] .k-layout-toolbar{right:0}[dir=rtl] .k-layout-toolbar{left:0}[dir=ltr] .k-layout-toolbar{border-left:1px solid var(--color-light)}[dir=rtl] .k-layout-toolbar{border-right:1px solid var(--color-light)}.k-layout-toolbar{position:absolute;top:0;bottom:0;width:var(--layout-toolbar-width);display:flex;flex-direction:column;font-size:var(--text-sm);background:var(--color-gray-100);color:var(--color-gray-500)}.k-layout-toolbar:hover{color:var(--color-black)}.k-layout-toolbar-button{width:var(--layout-toolbar-width);height:var(--layout-toolbar-width)}.k-layout-toolbar .k-sort-handle{margin-top:auto;color:currentColor}.k-layout-columns.k-grid{grid-gap:1px;background:var(--layout-border-color);background:var(--color-gray-300)}.k-layout:not(:first-child) .k-layout-columns.k-grid{border-top:0}.k-layouts .k-sortable-ghost{position:relative;box-shadow:#11111140 0 5px 10px;outline:2px solid var(--color-focus);cursor:grabbing;cursor:-webkit-grabbing;z-index:1}.k-layout-selector.k-dialog{background:#313740;color:var(--color-white)}.k-layout-selector .k-headline{line-height:1;margin-top:-.25rem;margin-bottom:1.5rem}.k-layout-selector ul{display:grid;grid-template-columns:repeat(3,1fr);grid-gap:1.5rem}.k-layout-selector-option .k-grid{height:5rem;grid-gap:2px;box-shadow:var(--shadow);cursor:pointer}.k-layout-selector-option:hover{outline:2px solid var(--color-green-300);outline-offset:2px}.k-layout-selector-option:last-child{margin-bottom:0}.k-layout-selector-option .k-column{display:flex;background:rgba(255,255,255,.2);justify-content:center;font-size:var(--text-xs);align-items:center}.k-layout-add-button{display:flex;align-items:center;width:100%;color:var(--color-gray-500);justify-content:center;padding:.75rem 0}.k-layout-add-button:hover{color:var(--color-black)}.k-line-field{position:relative;border:0;height:3rem;width:auto}.k-line-field:after{position:absolute;content:"";top:50%;margin-top:-1px;left:0;right:0;height:1px;background:var(--color-border)}.k-list-field .k-list-input{padding:.375rem .5rem .375rem .75rem}.k-pages-field[data-disabled=true] *{pointer-events:all!important}.k-structure-field{--item-height:38px}.k-structure-table{position:relative;table-layout:fixed;width:100%;background:#fff;font-size:var(--text-sm);border-spacing:0;box-shadow:var(--shadow)}[dir=ltr] .k-structure-table td,[dir=ltr] .k-structure-table th{border-right:1px solid var(--color-background)}[dir=rtl] .k-structure-table td,[dir=rtl] .k-structure-table th{border-left:1px solid var(--color-background)}.k-structure-table td,.k-structure-table th{line-height:1.25em;overflow:hidden;text-overflow:ellipsis}.k-structure-table th,.k-structure-table tr:not(:last-child) td{border-bottom:1px solid var(--color-background)}.k-structure-table td:last-child{overflow:visible}[dir=ltr] .k-structure-table th{text-align:left}[dir=rtl] .k-structure-table th{text-align:right}.k-structure-table th{position:sticky;top:0;left:0;right:0;width:100%;height:var(--item-height);padding:0 .75rem;background:#fff;color:var(--color-gray-600);font-weight:400;z-index:1}[dir=ltr] .k-structure-table td:last-child,[dir=ltr] .k-structure-table th:last-child{border-right:0}[dir=rtl] .k-structure-table td:last-child,[dir=rtl] .k-structure-table th:last-child{border-left:0}.k-structure-table td:last-child,.k-structure-table th:last-child{width:var(--item-height)}.k-structure-table tbody tr:hover td{background:rgba(239,239,239,.25)}@media screen and (max-width:65em){.k-structure-table td,.k-structure-table th{display:none}.k-structure-table td:first-child,.k-structure-table td:last-child,.k-structure-table td:nth-child(2),.k-structure-table th:first-child,.k-structure-table th:last-child,.k-structure-table th:nth-child(2){display:table-cell}}.k-structure-table .k-structure-table-column[data-align]{text-align:var(--align)}.k-structure-table .k-structure-table-column[data-align=right]>.k-input{flex-direction:column;align-items:flex-end}.k-structure-table .k-sort-handle,.k-structure-table .k-structure-table-index,.k-structure-table .k-structure-table-options,.k-structure-table .k-structure-table-options-button{width:var(--item-height);height:var(--item-height)}.k-structure-table .k-structure-table-index{text-align:center}.k-structure-table .k-structure-table-index-number{font-size:var(--text-xs);color:var(--color-gray-500);padding-top:.15rem}.k-structure-table .k-sort-handle,.k-structure-table[data-sortable=true] tr:hover .k-structure-table-index-number{display:none}.k-structure-table[data-sortable=true] tr:hover .k-sort-handle{display:flex!important}.k-structure-table .k-structure-table-options{position:relative;text-align:center}.k-structure-table .k-structure-table-text{padding:0 .75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.k-structure-table .k-sortable-ghost{background:var(--color-white);box-shadow:#11111140 0 5px 10px;outline:2px solid var(--color-focus);margin-bottom:2px;cursor:grabbing;cursor:-webkit-grabbing}[data-disabled=true] .k-structure-table{background:var(--color-background)}[dir=ltr] [data-disabled=true] .k-structure-table td,[dir=ltr] [data-disabled=true] .k-structure-table th{border-right:1px solid var(--color-border)}[dir=rtl] [data-disabled=true] .k-structure-table td,[dir=rtl] [data-disabled=true] .k-structure-table th{border-left:1px solid var(--color-border)}[data-disabled=true] .k-structure-table td,[data-disabled=true] .k-structure-table th{background:var(--color-background);border-bottom:1px solid var(--color-border)}[data-disabled=true] .k-structure-table td:last-child{overflow:hidden;text-overflow:ellipsis}.k-structure-table .k-sortable-row-fallback{opacity:0!important}.k-structure-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:2;height:100vh}.k-structure-form{position:relative;z-index:3;border-radius:var(--rounded-xs);margin-bottom:1px;box-shadow:#1111110d 0 0 0 3px;border:1px solid var(--color-border);background:var(--color-background)}.k-structure-form-fields{padding:1.5rem 1.5rem 2rem}.k-structure-form-buttons{border-top:1px solid var(--color-border);display:flex;justify-content:space-between}.k-structure-form-buttons .k-pagination{display:none}@media screen and (min-width:65em){.k-structure-form-buttons .k-pagination{display:flex}}.k-structure-form-buttons .k-pagination>.k-button,.k-structure-form-buttons .k-pagination>span{padding:.875rem 1rem!important}.k-structure-form-cancel-button,.k-structure-form-submit-button{padding:.875rem 1.5rem;line-height:1rem;display:flex}.k-field-counter{display:none}.k-text-field:focus-within .k-field-counter{display:block}.k-users-field[data-disabled=true] *{pointer-events:all!important}.k-writer-field-input{line-height:1.5em;padding:.375rem .5rem}.k-toolbar{background:var(--color-white);border-bottom:1px solid var(--color-background);height:38px}.k-toolbar-wrapper{position:absolute;top:0;left:0;right:0;max-width:100%}.k-toolbar-buttons{display:flex}.k-toolbar-divider{width:1px;background:var(--color-background)}.k-toolbar-button{width:36px;height:36px}.k-toolbar-button:hover{background:rgba(239,239,239,.5)}.k-date-field-preview{padding:0 .75rem}.k-url-field-preview{padding:0 .75rem;overflow:hidden;text-overflow:ellipsis}.k-url-field-preview a{color:var(--color-focus);text-decoration:underline;transition:color .3s;white-space:nowrap;max-width:100%}.k-url-field-preview a:hover{color:var(--color-black)}.k-files-field-preview{display:grid;grid-gap:.5rem;grid-template-columns:repeat(auto-fill,1.525rem);padding:0 .75rem}.k-files-field-preview li{line-height:0}.k-files-field-preview li .k-icon{--size:.85rem;height:100%}.k-list-field-preview{padding:.325rem .75rem;line-height:1.5em}[dir=ltr] .k-list-field-preview ol,[dir=ltr] .k-list-field-preview ul{margin-left:1rem}[dir=rtl] .k-list-field-preview ol,[dir=rtl] .k-list-field-preview ul{margin-right:1rem}.k-list-field-preview ul>li{list-style:disc}.k-list-field-preview ol ul>li,.k-list-field-preview ul ul>li{list-style:circle}.k-list-field-preview ol>li{list-style:decimal}.k-list-field-preview ol>li::marker{color:var(--color-gray-500);font-size:var(--text-xs)}.k-pages-field-preview{padding:0 .25rem 0 .75rem;display:flex}[dir=ltr] .k-pages-field-preview li{margin-right:.5rem}[dir=rtl] .k-pages-field-preview li{margin-left:.5rem}.k-pages-field-preview li{line-height:0}.k-pages-field-preview .k-link{display:flex;align-items:stretch;background:var(--color-background);box-shadow:var(--shadow)}.k-pages-field-preview-image{width:1.525rem;height:1.525rem}.k-pages-field-preview-image .k-icon{--size:.85rem}[dir=ltr] .k-pages-field-preview figcaption{border-left:0}[dir=rtl] .k-pages-field-preview figcaption{border-right:0}.k-pages-field-preview figcaption{flex-grow:1;line-height:1.5em;padding:0 .5rem;border:1px solid var(--color-border);border-radius:var(--rounded-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-time-field-preview{padding:0 .75rem}.k-toggle-field-preview label{padding:0 .25rem 0 .75rem;display:flex;height:38px;cursor:pointer;overflow:hidden;white-space:nowrap}[dir=ltr] .k-toggle-field-preview .k-toggle-input-label{padding-left:.5rem}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input-label,[dir=rtl] .k-toggle-field-preview .k-toggle-input-label{padding-right:.5rem}[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input-label{padding-left:.5rem}[dir=ltr] .k-toggle-field-preview .k-toggle-input{padding-left:.75rem;padding-right:.25rem}.k-toggle-field-preview .k-toggle-input{padding-top:0;padding-bottom:0}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input,[dir=rtl] .k-toggle-field-preview .k-toggle-input{padding-left:.25rem;padding-right:.75rem}[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input{padding-right:.25rem;padding-left:.75rem}[data-align=right] .k-toggle-field-preview .k-toggle-input{flex-direction:row-reverse}.k-users-field-preview{padding:0 .25rem 0 .75rem;display:flex}[dir=ltr] .k-users-field-preview li{margin-right:.5rem}[dir=rtl] .k-users-field-preview li{margin-left:.5rem}.k-users-field-preview li{line-height:0}.k-users-field-preview .k-link{display:flex;align-items:stretch;background:var(--color-background);box-shadow:var(--shadow)}.k-users-field-preview-avatar{width:1.525rem;height:1.525rem;color:var(--color-gray-500)!important}.k-users-field-preview-avatar.k-image{display:block}[dir=ltr] .k-users-field-preview figcaption{border-left:0}[dir=rtl] .k-users-field-preview figcaption{border-right:0}.k-users-field-preview figcaption{flex-grow:1;line-height:1.5em;padding-left:.5rem;padding-right:.5rem;border:1px solid var(--color-border);border-radius:var(--rounded-xs);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-writer-field-preview{padding:.325rem .75rem;line-height:1.5em}.k-writer-field-preview p:not(:last-child){margin-bottom:1.5em}.k-aspect-ratio{position:relative;display:block;overflow:hidden;padding-bottom:100%}.k-aspect-ratio>*{position:absolute!important;top:0;right:0;bottom:0;left:0;height:100%;width:100%;-o-object-fit:contain;object-fit:contain}.k-aspect-ratio[data-cover=true]>*{-o-object-fit:cover;object-fit:cover}.k-bar{display:flex;align-items:center;justify-content:space-between;line-height:1}.k-bar-slot{flex-grow:1}.k-bar-slot[data-position=center]{text-align:center}[dir=ltr] .k-bar-slot[data-position=right]{text-align:right}[dir=rtl] .k-bar-slot[data-position=right]{text-align:left}.k-box{word-wrap:break-word;font-size:var(--text-sm)}[dir=ltr] .k-box:not([data-theme=none]){border-left:2px solid var(--color-gray-500)}[dir=rtl] .k-box:not([data-theme=none]){border-right:2px solid var(--color-gray-500)}.k-box:not([data-theme=none]){background:var(--color-gray-300);border-radius:var(--rounded-xs);line-height:1.25rem;padding:.5rem 1.5rem}.k-box[data-theme=code]{background:var(--color-gray-900);border:1px solid var(--color-black);color:var(--color-light);font-family:Input,Menlo,monospace;font-size:var(--text-sm);line-height:1.5}.k-box[data-theme=button]{padding:0}[dir=ltr] .k-box[data-theme=button] .k-button{text-align:left}[dir=rtl] .k-box[data-theme=button] .k-button{text-align:right}.k-box[data-theme=button] .k-button{padding:0 .75rem;height:2.25rem;width:100%;display:flex;align-items:center;line-height:2rem}[dir=ltr] .k-box[data-theme=info],[dir=ltr] .k-box[data-theme=negative],[dir=ltr] .k-box[data-theme=notice],[dir=ltr] .k-box[data-theme=positive]{border-left-color:var(--theme-light)}[dir=rtl] .k-box[data-theme=info],[dir=rtl] .k-box[data-theme=negative],[dir=rtl] .k-box[data-theme=notice],[dir=rtl] .k-box[data-theme=positive]{border-right-color:var(--theme-light)}.k-box[data-theme=info],.k-box[data-theme=negative],.k-box[data-theme=notice],.k-box[data-theme=positive]{border:0;background:var(--theme-bg)}[dir=ltr] .k-box[data-theme=empty]{border-left:0}[dir=rtl] .k-box[data-theme=empty]{border-right:0}.k-box[data-theme=empty]{text-align:center;padding:3rem 1.5rem;display:flex;justify-content:center;align-items:center;flex-direction:column;background:var(--color-background);border:1px dashed var(--color-border)}.k-box[data-theme=empty] .k-icon{margin-bottom:.5rem;color:var(--color-gray-500)}.k-box[data-theme=empty],.k-box[data-theme=empty] p{color:var(--color-gray-600)}.k-collection-help{padding:.5rem .75rem}.k-collection-footer{display:flex;justify-content:space-between;margin-left:-.75rem;margin-right:-.75rem}.k-collection-pagination{line-height:1.25rem;flex-shrink:0;min-height:2.75rem}.k-collection-pagination .k-pagination .k-button{padding:.5rem .75rem;line-height:1.125rem}.k-column{min-width:0;grid-column-start:span 12}.k-column[data-sticky=true]>div{position:sticky;top:4vh;z-index:2}@media screen and (min-width:65em){.k-column[data-width="1/1"],.k-column[data-width="12/12"],.k-column[data-width="2/2"],.k-column[data-width="3/3"],.k-column[data-width="4/4"],.k-column[data-width="6/6"]{grid-column-start:span 12}.k-column[data-width="11/12"]{grid-column-start:span 11}.k-column[data-width="10/12"],.k-column[data-width="5/6"]{grid-column-start:span 10}.k-column[data-width="3/4"],.k-column[data-width="9/12"]{grid-column-start:span 9}.k-column[data-width="2/3"],.k-column[data-width="4/6"],.k-column[data-width="8/12"]{grid-column-start:span 8}.k-column[data-width="7/12"]{grid-column-start:span 7}.k-column[data-width="1/2"],.k-column[data-width="2/4"],.k-column[data-width="3/6"],.k-column[data-width="6/12"]{grid-column-start:span 6}.k-column[data-width="5/12"]{grid-column-start:span 5}.k-column[data-width="1/3"],.k-column[data-width="2/6"],.k-column[data-width="4/12"]{grid-column-start:span 4}.k-column[data-width="1/4"],.k-column[data-width="3/12"]{grid-column-start:span 3}.k-column[data-width="1/6"],.k-column[data-width="2/12"]{grid-column-start:span 2}.k-column[data-width="1/12"]{grid-column-start:span 1}}.k-column[data-disabled=true]{cursor:not-allowed;opacity:.4}.k-column[data-disabled=true] *{pointer-events:none}.k-column[data-disabled=true] .k-text[data-theme=help] *{pointer-events:initial}.k-dropzone{position:relative}.k-dropzone:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;display:none;pointer-events:none;z-index:1}.k-dropzone[data-over=true]:after{display:block;outline:1px solid var(--color-focus);box-shadow:var(--color-focus-outline) 0 0 0 3px}.k-empty{display:flex;align-items:stretch;border-radius:var(--rounded-xs);color:var(--color-gray-600);border:1px dashed var(--color-border)}button.k-empty{width:100%}button.k-empty:focus{outline:0}.k-empty p{font-size:var(--text-sm);color:var(--color-gray-600)}.k-empty>.k-icon{color:var(--color-gray-500)}.k-empty[data-layout=cardlets],.k-empty[data-layout=cards]{text-align:center;padding:1.5rem;justify-content:center;flex-direction:column}.k-empty[data-layout=cardlets] .k-icon,.k-empty[data-layout=cards] .k-icon{margin-bottom:1rem}.k-empty[data-layout=cardlets] .k-icon svg,.k-empty[data-layout=cards] .k-icon svg{width:2rem;height:2rem}.k-empty[data-layout=list]{min-height:38px}[dir=ltr] .k-empty[data-layout=list]>.k-icon{border-right:1px solid rgba(0,0,0,.05)}[dir=rtl] .k-empty[data-layout=list]>.k-icon{border-left:1px solid rgba(0,0,0,.05)}.k-empty[data-layout=list]>.k-icon{width:36px;min-height:36px}.k-empty[data-layout=list]>p{line-height:1.25rem;padding:.5rem .75rem}.k-file-preview{background:var(--color-gray-800)}.k-file-preview-layout{display:grid;grid-template-columns:50%auto}.k-file-preview-layout>*{min-width:0}.k-file-preview-image{position:relative;display:flex;align-items:center;justify-content:center;background:var(--bg-pattern)}.k-file-preview-image-link{display:block;width:100%;padding:min(4vw,3rem);outline:0}.k-file-preview-image-link[data-tabbed=true]{box-shadow:none;outline:2px solid var(--color-focus);outline-offset:-2px}.k-file-preview-details{padding:1.5rem;flex-grow:1}.k-file-preview-details ul{line-height:1.5em;max-width:50rem;display:grid;grid-gap:1.5rem 3rem;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.k-file-preview-details h3{font-size:var(--text-sm);font-weight:500;color:var(--color-gray-500)}.k-file-preview-details a,.k-file-preview-details p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#ffffffbf;font-size:var(--text-sm)}@media screen and (min-width:30em){.k-file-preview-details ul{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media screen and (max-width:65em){.k-file-preview-layout{padding:0!important}}@media screen and (min-width:65em){.k-file-preview-layout{grid-template-columns:33.333%auto;align-items:center}.k-file-preview-details{padding:3rem}}@media screen and (min-width:90em){.k-file-preview-layout{grid-template-columns:25%auto}}.k-grid{--columns:12;display:grid;grid-column-gap:0;grid-row-gap:0;grid-template-columns:1fr}@media screen and (min-width:30em){.k-grid[data-gutter=small]{grid-column-gap:1rem;grid-row-gap:1rem}.k-grid[data-gutter=huge],.k-grid[data-gutter=large],.k-grid[data-gutter=medium]{grid-column-gap:1.5rem;grid-row-gap:1.5rem}}@media screen and (min-width:65em){.k-grid{grid-template-columns:repeat(var(--columns),1fr)}.k-grid[data-gutter=large]{grid-column-gap:3rem}.k-grid[data-gutter=huge]{grid-column-gap:4.5rem}}@media screen and (min-width:90em){.k-grid[data-gutter=large]{grid-column-gap:4.5rem}.k-grid[data-gutter=huge]{grid-column-gap:6rem}}@media screen and (min-width:120em){.k-grid[data-gutter=large]{grid-column-gap:6rem}.k-grid[data-gutter=huge]{grid-column-gap:7.5rem}}.k-header{padding-top:4vh;margin-bottom:2rem;border-bottom:1px solid var(--color-border)}.k-header .k-headline{min-height:1.25em;margin-bottom:.5rem;word-wrap:break-word}.k-header .k-header-buttons{margin-top:-.5rem;height:3.25rem}.k-header .k-headline-editable{cursor:pointer}[dir=ltr] .k-header .k-headline-editable .k-icon{margin-left:.5rem}[dir=rtl] .k-header .k-headline-editable .k-icon{margin-right:.5rem}.k-header .k-headline-editable .k-icon{color:var(--color-gray-500);opacity:0;transition:opacity .3s;display:inline-block}.k-header .k-headline-editable:hover .k-icon{opacity:1}.k-panel-inside{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column}.k-panel-inside:focus{outline:0}.k-panel-header{z-index:var(--z-navigation);flex-shrink:0}.k-panel-view{flex-grow:1;padding-bottom:6rem}.k-item{position:relative;background:var(--color-white);border-radius:var(--rounded-sm);box-shadow:var(--shadow);display:grid;grid-template-columns:auto;line-height:1}.k-item a:focus,.k-item:focus{outline:0}.k-item:focus-within{box-shadow:var(--shadow-outline)}.k-item-sort-handle.k-sort-handle{position:absolute;opacity:0;width:1.25rem;height:1.5rem;z-index:2;border-radius:1px}.k-item:hover .k-item-sort-handle{opacity:1}.k-item-figure{grid-area:figure}.k-item-content{grid-area:content;overflow:hidden}.k-item-info,.k-item-title{font-size:var(--text-sm);font-weight:400;text-overflow:ellipsis;white-space:nowrap;line-height:1.125rem;overflow:hidden}.k-item-info{grid-area:info;color:var(--color-gray-500)}.k-item-title-link.k-link[data-=true]{box-shadow:none}.k-item-title-link:after{position:absolute;content:"";top:0;right:0;bottom:0;left:0;z-index:1}.k-item-footer{grid-area:footer;display:flex;justify-content:space-between;align-items:center;min-width:0}[dir=ltr] .k-item-label{margin-right:.5rem}[dir=rtl] .k-item-label{margin-left:.5rem}.k-item-buttons{position:relative;display:flex;justify-content:flex-end;flex-shrink:0;flex-grow:1}.k-item-buttons>.k-button,.k-item-buttons>.k-dropdown{position:relative;width:38px;height:38px;display:flex!important;align-items:center;justify-content:center;line-height:1}.k-item-buttons>.k-button{z-index:1}.k-item-buttons>.k-options-dropdown>.k-options-dropdown-toggle{z-index:var(--z-toolbar)}.k-list-item{display:flex;align-items:center;height:38px}[dir=ltr] .k-list-item .k-item-sort-handle{left:-1.5rem}[dir=rtl] .k-list-item .k-item-sort-handle{right:-1.5rem}.k-list-item .k-item-sort-handle{width:1.5rem}[dir=ltr] .k-list-item .k-item-figure{border-top-left-radius:var(--rounded-sm)}[dir=rtl] .k-list-item .k-item-figure{border-top-right-radius:var(--rounded-sm)}[dir=ltr] .k-list-item .k-item-figure{border-bottom-left-radius:var(--rounded-sm)}[dir=rtl] .k-list-item .k-item-figure{border-bottom-right-radius:var(--rounded-sm)}.k-list-item .k-item-figure{width:38px}[dir=ltr] .k-list-item .k-item-content{margin-left:.75rem}[dir=rtl] .k-list-item .k-item-content{margin-right:.75rem}.k-list-item .k-item-content{display:flex;flex-grow:1;flex-shrink:2;justify-content:space-between;align-items:center}.k-list-item .k-item-info,.k-list-item .k-item-title{flex-grow:1;line-height:1.5rem}[dir=ltr] .k-list-item .k-item-title{margin-right:.5rem}[dir=rtl] .k-list-item .k-item-title{margin-left:.5rem}.k-list-item .k-item-title{flex-shrink:1}[dir=ltr] .k-list-item .k-item-info{text-align:right}[dir=rtl] .k-list-item .k-item-info{text-align:left}[dir=ltr] .k-list-item .k-item-info{margin-right:.5rem}[dir=rtl] .k-list-item .k-item-info{margin-left:.5rem}.k-list-item .k-item-info{flex-shrink:2;justify-self:end}.k-list-item .k-item-buttons,.k-list-item .k-item-footer{flex-shrink:0}.k-item:not(.k-list-item) .k-item-sort-handle{margin:.25rem;background:var(--color-background);box-shadow:var(--shadow-md)}[dir=ltr] .k-item:not(.k-list-item) .k-item-label{margin-left:-2px}[dir=rtl] .k-item:not(.k-list-item) .k-item-label{margin-right:-2px}.k-item:not(.k-list-item) .k-item-content{padding:.625rem .75rem}.k-cardlets-item{height:6rem;grid-template-rows:auto 38px;grid-template-areas:"content""footer"}.k-cardlets-item[data-has-figure=true]{grid-template-columns:6rem auto;grid-template-areas:"figure content""figure footer"}[dir=ltr] .k-cardlets-item .k-item-figure{border-top-left-radius:var(--rounded-sm)}[dir=rtl] .k-cardlets-item .k-item-figure{border-top-right-radius:var(--rounded-sm)}[dir=ltr] .k-cardlets-item .k-item-figure{border-bottom-left-radius:var(--rounded-sm)}[dir=rtl] .k-cardlets-item .k-item-figure{border-bottom-right-radius:var(--rounded-sm)}.k-cardlets-item .k-item-footer{padding-top:.5rem;padding-bottom:.5rem}.k-cards-item{grid-template-columns:auto;grid-template-rows:auto 1fr;grid-template-areas:"figure""content";--item-content-wrapper:0}[dir=ltr] .k-cards-item .k-item-figure,[dir=rtl] .k-cards-item .k-item-figure{border-top-right-radius:var(--rounded-sm);border-top-left-radius:var(--rounded-sm)}.k-cards-item .k-item-content{padding:.5rem .75rem!important;overflow:hidden}.k-cards-item .k-item-info,.k-cards-item .k-item-title{line-height:1.375rem;white-space:normal}.k-cards-item .k-item-info:after,.k-cards-item .k-item-title:after{display:inline-block;content:"\a0";width:var(--item-content-wrapper)}.k-cards-item[data-has-flag=true],.k-cards-item[data-has-options=true]{--item-content-wrapper:38px}.k-cards-item[data-has-flag=true][data-has-options=true]{--item-content-wrapper:76px}.k-cards-item[data-has-info=true] .k-item-title:after{display:none}[dir=ltr] .k-cards-item .k-item-footer{right:0}[dir=rtl] .k-cards-item .k-item-footer{left:0}.k-cards-item .k-item-footer{position:absolute;bottom:0;width:auto}.k-item-figure{overflow:hidden;flex-shrink:0}.k-cards-items{--min:13rem;--max:1fr;--gap:1.5rem;--column-gap:var(--gap);--row-gap:var(--gap);display:grid;grid-column-gap:var(--column-gap);grid-row-gap:var(--row-gap);grid-template-columns:repeat(auto-fill,minmax(var(--min),var(--max)))}@media screen and (min-width:30em){.k-cards-items[data-size=tiny]{--min:10rem}.k-cards-items[data-size=small]{--min:16rem}.k-cards-items[data-size=medium]{--min:24rem}.k-cards-items[data-size=huge],.k-cards-items[data-size=large],.k-column[data-width="1/4"] .k-cards-items,.k-column[data-width="1/5"] .k-cards-items,.k-column[data-width="1/6"] .k-cards-items{--min:1fr}}@media screen and (min-width:65em){.k-cards-items[data-size=large]{--min:32rem}}.k-cardlets-items{display:grid;grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));grid-gap:.5rem}.k-list-items .k-list-item:not(:last-child){margin-bottom:2px}.k-overlay{position:fixed;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:var(--z-dialog);transform:translate(0)}.k-overlay[data-centered=true]{display:flex;align-items:center;justify-content:center}.k-overlay[data-dimmed=true]{background:var(--color-backdrop)}.k-overlay-loader{color:var(--color-white)}.k-panel[data-loading=true]{animation:LoadingCursor .5s}.k-panel[data-dragging=true],.k-panel[data-loading=true]:after{-webkit-user-select:none;-moz-user-select:none;user-select:none}.k-tabs{position:relative;background:#e9e9e9;border-top:1px solid var(--color-border);border-left:1px solid var(--color-border);border-right:1px solid var(--color-border)}.k-tabs nav{display:flex;justify-content:center;margin-left:-1px;margin-right:-1px}[dir=ltr] .k-tab-button.k-button{border-left:1px solid transparent}[dir=rtl] .k-tab-button.k-button{border-right:1px solid transparent}[dir=ltr] .k-tab-button.k-button{border-right:1px solid var(--color-border)}[dir=rtl] .k-tab-button.k-button{border-left:1px solid var(--color-border)}.k-tab-button.k-button{position:relative;z-index:1;display:inline-flex;justify-content:center;align-items:center;padding:.625rem .75rem;font-size:var(--text-xs);text-transform:uppercase;text-align:center;font-weight:500;flex-grow:1;flex-shrink:1;flex-direction:column;max-width:15rem}@media screen and (min-width:30em){.k-tab-button.k-button{flex-direction:row}[dir=ltr] .k-tab-button.k-button .k-icon{margin-right:.5rem}[dir=rtl] .k-tab-button.k-button .k-icon{margin-left:.5rem}}[dir=ltr] .k-tab-button.k-button>.k-button-text{padding-left:0}[dir=rtl] .k-tab-button.k-button>.k-button-text{padding-right:0}.k-tab-button.k-button>.k-button-text{padding-top:.375rem;font-size:10px;overflow:hidden;max-width:10rem;text-overflow:ellipsis;opacity:1}@media screen and (min-width:30em){.k-tab-button.k-button>.k-button-text{font-size:var(--text-xs);padding-top:0}}[dir=ltr] .k-tab-button:last-child{border-right:1px solid transparent}[dir=rtl] .k-tab-button:last-child{border-left:1px solid transparent}[dir=ltr] .k-tab-button[aria-current]{border-right:1px solid var(--color-border)}[dir=rtl] .k-tab-button[aria-current]{border-left:1px solid var(--color-border)}.k-tab-button[aria-current]{position:relative;background:var(--color-background);pointer-events:none}[dir=ltr] .k-tab-button[aria-current]:first-child{border-left:1px solid var(--color-border)}[dir=rtl] .k-tab-button[aria-current]:first-child{border-right:1px solid var(--color-border)}.k-tab-button[aria-current]:after,.k-tab-button[aria-current]:before{position:absolute;content:""}.k-tab-button[aria-current]:before{left:-1px;right:-1px;top:-1px;height:2px;background:var(--color-black)}.k-tab-button[aria-current]:after{left:0;right:0;bottom:-1px;height:1px;background:var(--color-background)}[dir=ltr] .k-tabs-dropdown{right:0}[dir=rtl] .k-tabs-dropdown{left:0}.k-tabs-dropdown{top:100%}[dir=ltr] .k-tabs-badge{right:2px}[dir=rtl] .k-tabs-badge{left:2px}.k-tabs-badge{position:absolute;top:3px;font-variant-numeric:tabular-nums;line-height:1.5;padding:0 .25rem;border-radius:2px;font-size:10px;box-shadow:var(--shadow-md)}.k-tabs[data-theme=notice] .k-tabs-badge{background:var(--theme-light);color:var(--color-black)}.k-view{padding-left:1.5rem;padding-right:1.5rem;margin:0 auto;max-width:100rem}@media screen and (min-width:30em){.k-view{padding-left:3rem;padding-right:3rem}}@media screen and (min-width:90em){.k-view{padding-left:6rem;padding-right:6rem}}.k-view[data-align=center]{height:100vh;display:flex;align-items:center;justify-content:center;padding:0 3rem;overflow:auto}.k-view[data-align=center]>*{flex-basis:22.5rem}.k-fatal{position:fixed;top:0;right:0;bottom:0;left:0;background:var(--color-backdrop);display:flex;z-index:var(--z-fatal);align-items:center;justify-content:center;padding:1.5rem}.k-fatal-box{width:100%;height:100%;display:flex;flex-direction:column;color:var(--color-black);background:var(--color-red-400);box-shadow:var(--shadow-xl);border-radius:var(--rounded)}.k-fatal-box .k-headline{line-height:1;font-size:var(--text-sm);padding:.75rem}.k-fatal-box .k-button{padding:.75rem}.k-fatal-iframe{border:0;width:100%;flex-grow:1;background:var(--color-white)}.k-headline{--size:var(--text-base);font-size:var(--size);font-weight:var(--font-bold);line-height:1.5em}.k-headline[data-size=small]{--size:var(--text-sm)}.k-headline[data-size=large]{--size:var(--text-xl);font-weight:var(--font-normal)}@media screen and (min-width:65em){.k-headline[data-size=large]{--size:var(--text-2xl)}}.k-headline[data-size=huge]{--size:var(--text-2xl);line-height:1.15em}@media screen and (min-width:65em){.k-headline[data-size=huge]{--size:var(--text-3xl)}}.k-headline[data-theme]{color:var(--theme)}[dir=ltr] .k-headline abbr{padding-left:.25rem}[dir=rtl] .k-headline abbr{padding-right:.25rem}.k-headline abbr{color:var(--color-gray-500);text-decoration:none}.k-icon{--size:1rem;position:relative;line-height:0;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:var(--size)}.k-icon[data-size=medium]{--size:2rem}.k-icon[data-size=large]{--size:3rem}.k-icon svg{width:var(--size);height:var(--size);-moz-transform:scale(1)}.k-icon svg *{fill:currentColor}.k-icon[data-back=black]{color:var(--color-white)}.k-icon[data-back=white]{color:var(--color-gray-900)}.k-icon[data-back=pattern]{color:var(--color-white)}[data-disabled=true] .k-icon[data-back=pattern] svg{opacity:1}.k-icon-emoji{display:block;line-height:1;font-style:normal;font-size:var(--size)}@media only screen and (-webkit-min-device-pixel-ratio:2),not all,only screen and (min-resolution:192dpi),only screen and (min-resolution:2dppx){.k-icon-emoji{font-size:1.25em}}.k-icons{position:absolute;width:0;height:0}.k-image span{position:relative;display:block;line-height:0;padding-bottom:100%}.k-image img{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;-o-object-fit:contain;object-fit:contain}[dir=ltr] .k-image-error{left:50%}[dir=rtl] .k-image-error{right:50%}.k-image-error{position:absolute;top:50%;transform:translate(-50%,-50%);color:var(--color-white);font-size:.9em}.k-image-error svg *{fill:#ffffff4d}.k-image[data-cover=true] img{-o-object-fit:cover;object-fit:cover}.k-image[data-back=black] span{background:var(--color-gray-900)}.k-image[data-back=white] span{background:var(--color-white);color:var(--color-gray-900)}.k-image[data-back=white] .k-image-error{background:var(--color-gray-900);color:var(--color-white)}.k-image[data-back=pattern] span{background:var(--color-gray-800) var(--bg-pattern)}.k-loader{z-index:1}.k-loader-icon{animation:Spin .9s linear infinite}.k-offline-warning{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--z-offline);background:var(--color-backdrop);display:flex;align-items:center;justify-content:center;line-height:1}.k-offline-warning p{display:flex;align-items:center;gap:.5rem;background:var(--color-white);box-shadow:var(--shadow);padding:.75rem;border-radius:var(--rounded)}.k-offline-warning p .k-icon{color:var(--color-red-400)}.k-progress{-webkit-appearance:none;width:100%;height:.5rem;border-radius:5rem;background:var(--color-border);overflow:hidden;border:0}.k-progress::-webkit-progress-bar{border:0;background:var(--color-border);height:.5rem;border-radius:20px}.k-progress::-webkit-progress-value{border-radius:inherit;background:var(--color-focus);-webkit-transition:width .3s;transition:width .3s}.k-progress::-moz-progress-bar{border-radius:inherit;background:var(--color-focus);-moz-transition:width .3s;transition:width .3s}[dir=ltr] .k-registration,[dir=ltr] .k-registration p{margin-right:1rem}[dir=rtl] .k-registration,[dir=rtl] .k-registration p{margin-left:1rem}.k-registration{display:flex;align-items:center}.k-registration p{color:var(--color-negative-light);font-size:var(--text-sm);font-weight:600}@media screen and (max-width:90em){.k-registration p{display:none}}.k-registration .k-button{color:var(--color-white)}.k-sort-handle{cursor:move;cursor:grab;cursor:-webkit-grab;color:var(--color-gray-900);justify-content:center;align-items:center;line-height:0;width:2rem;height:2rem;display:flex;will-change:opacity,color;transition:opacity .3s;z-index:1}.k-sort-handle svg{width:1rem;height:1rem}.k-sort-handle:active{cursor:grabbing;cursor:-webkit-grabbing}.k-status-icon svg{width:14px;height:14px}.k-status-icon .k-icon{color:var(--theme-light)}.k-status-icon .k-button-text{color:var(--color-black)}.k-status-icon[data-disabled=true]{opacity:1!important}.k-status-icon[data-disabled=true] .k-icon{color:var(--color-gray-400);opacity:.5}.k-text{line-height:1.5em}[dir=ltr] .k-text ol,[dir=ltr] .k-text ul{margin-left:1rem}[dir=rtl] .k-text ol,[dir=rtl] .k-text ul{margin-right:1rem}.k-text li{list-style:inherit}.k-text p,.k-text>ol,.k-text>ul{margin-bottom:1.5em}.k-text a{text-decoration:underline}.k-text>:last-child{margin-bottom:0}.k-text[data-size=tiny]{font-size:var(--text-xs)}.k-text[data-size=small]{font-size:var(--text-sm)}.k-text[data-size=medium]{font-size:var(--text-base)}.k-text[data-size=large]{font-size:var(--text-xl)}.k-text[data-align]{text-align:var(--align)}.k-text[data-theme=help]{font-size:var(--text-sm);color:var(--color-gray-600);line-height:1.25rem}.k-dialog-body .k-text{word-wrap:break-word}.k-user-info{display:flex;align-items:center;line-height:1;font-size:var(--text-sm)}[dir=ltr] .k-user-info .k-image{margin-right:.75rem}[dir=rtl] .k-user-info .k-image{margin-left:.75rem}.k-user-info .k-image{width:1.5rem}[dir=ltr] .k-user-info .k-icon{margin-right:.75rem}[dir=rtl] .k-user-info .k-icon{margin-left:.75rem}.k-user-info .k-icon{width:1.5rem;height:1.5rem;background:var(--color-black);color:var(--color-white)}.k-breadcrumb{padding-left:.5rem;padding-right:.5rem}.k-breadcrumb-dropdown{height:2.5rem;width:2.5rem;display:flex;align-items:center;justify-content:center}.k-breadcrumb ol{display:none;align-items:center}@media screen and (min-width:30em){.k-breadcrumb ol{display:flex}.k-breadcrumb-dropdown{display:none}}.k-breadcrumb li,.k-breadcrumb-link{display:flex;align-items:center;min-width:0}.k-breadcrumb-link{font-size:var(--text-sm);align-self:stretch;padding-top:.625rem;padding-bottom:.625rem;line-height:1.25rem}.k-breadcrumb li{flex-shrink:3}.k-breadcrumb li:last-child{flex-shrink:1}.k-breadcrumb li:not(:last-child):after{content:"/";padding-left:.5rem;padding-right:.5rem;opacity:.5;flex-shrink:0}.k-breadcrumb li:not(:first-child):not(:last-child){max-width:15vw}[dir=ltr] .k-breadcrumb-icon{margin-right:.5rem}[dir=rtl] .k-breadcrumb-icon{margin-left:.5rem}.k-breadcrumb-link-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}button{line-height:inherit;border:0;font-family:var(--font-sans);font-size:1rem;color:currentColor;background:0 0;cursor:pointer}button::-moz-focus-inner{padding:0;border:0}.k-button{display:inline-block;position:relative;font-size:var(--text-sm);transition:color .3s;outline:0}.k-button:focus,.k-button:hover{outline:0}.k-button *{vertical-align:middle}.k-button[data-responsive=true] .k-button-text{display:none}@media screen and (min-width:30em){.k-button[data-responsive=true] .k-button-text{display:inline}}.k-button[data-theme]{color:var(--theme)}.k-button-icon{display:inline-flex;align-items:center;line-height:0}[dir=ltr] .k-button-icon~.k-button-text{padding-left:.5rem}[dir=rtl] .k-button-icon~.k-button-text{padding-right:.5rem}.k-button-text{opacity:.75}.k-button:focus .k-button-text,.k-button:hover .k-button-text{opacity:1}.k-button-text b,.k-button-text span{vertical-align:baseline}.k-button[data-disabled=true]{opacity:.5;pointer-events:none;cursor:default}.k-card-options>.k-button[data-disabled=true]{display:inline-flex}.k-button-group{--padding-x:.75rem;--padding-y:1rem;--line-height:1rem;font-size:0;margin:0 calc(var(--padding-x)*-1)}.k-button-group>.k-dropdown{height:calc(var(--line-height) + (var(--padding-y)*2));display:inline-block}.k-button-group>.k-button,.k-button-group>.k-dropdown>.k-button{padding:var(--padding-y) var(--padding-x);line-height:var(--line-height)}.k-button-group .k-dropdown-content{top:calc(100% + 1px);margin:0 var(--padding-x)}.k-dropdown{position:relative}[dir=ltr] .k-dropdown-content{text-align:left}[dir=rtl] .k-dropdown-content{text-align:right}.k-dropdown-content{position:absolute;top:100%;background:var(--color-black);color:var(--color-white);z-index:var(--z-dropdown);box-shadow:var(--shadow-lg);border-radius:var(--rounded-xs);margin-bottom:6rem}[dir=ltr] .k-dropdown-content[data-align=left]{left:0}[dir=ltr] .k-dropdown-content[data-align=right],[dir=rtl] .k-dropdown-content[data-align=left]{right:0}[dir=rtl] .k-dropdown-content[data-align=right]{left:0}.k-dropdown-content>.k-dropdown-item:first-child{margin-top:.5rem}.k-dropdown-content>.k-dropdown-item:last-child{margin-bottom:.5rem}.k-dropdown-content[data-dropup=true]{top:auto;bottom:100%;margin-bottom:.5rem}.k-dropdown-content hr{border-color:currentColor;opacity:.2;margin:.5rem 1rem}.k-dropdown-content[data-theme=light]{background:var(--color-white);color:var(--color-black)}.k-dropdown-item{white-space:nowrap;line-height:1;display:flex;width:100%;align-items:center;font-size:var(--text-sm);padding:6px 16px}.k-dropdown-item:focus{outline:0;box-shadow:var(--shadow-outline)}[dir=ltr] .k-dropdown-item .k-button-figure{padding-right:.5rem}[dir=rtl] .k-dropdown-item .k-button-figure{padding-left:.5rem}.k-dropdown-item .k-button-figure{text-align:center}.k-link{outline:0}.k-options-dropdown,.k-options-dropdown-toggle{display:flex;justify-content:center;align-items:center;height:38px}.k-options-dropdown-toggle{min-width:38px;padding:0 .75rem}.k-pagination{-webkit-user-select:none;-moz-user-select:none;user-select:none;direction:ltr}.k-pagination .k-button{padding:1rem}.k-pagination-details{white-space:nowrap}.k-pagination>span{font-size:var(--text-sm)}.k-pagination[data-align]{text-align:var(--align)}[dir=ltr] .k-dropdown-content.k-pagination-selector{left:50%}[dir=rtl] .k-dropdown-content.k-pagination-selector{right:50%}.k-dropdown-content.k-pagination-selector{position:absolute;top:100%;transform:translate(-50%);background:var(--color-black)}[dir=ltr] .k-dropdown-content.k-pagination-selector{direction:ltr}[dir=rtl] .k-dropdown-content.k-pagination-selector{direction:rtl}.k-pagination-settings{display:flex;align-items:center;justify-content:space-between}.k-pagination-settings .k-button{line-height:1}[dir=ltr] .k-pagination-settings label{border-right:1px solid rgba(255,255,255,.35)}[dir=rtl] .k-pagination-settings label{border-left:1px solid rgba(255,255,255,.35)}.k-pagination-settings label{display:flex;align-items:center;padding:.625rem 1rem;font-size:var(--text-xs)}[dir=ltr] .k-pagination-settings label span{margin-right:.5rem}[dir=rtl] .k-pagination-settings label span{margin-left:.5rem}.k-prev-next{direction:ltr}.k-search{max-width:30rem;margin:2.5rem auto;box-shadow:var(--shadow-lg)}.k-search-input{background:var(--color-light);display:flex}.k-search-types{flex-shrink:0;display:flex}[dir=ltr] .k-search-types>.k-button{padding-left:1rem}[dir=rtl] .k-search-types>.k-button{padding-right:1rem}.k-search-types>.k-button{font-size:var(--text-base);line-height:1;height:2.5rem}.k-search-types>.k-button .k-icon{height:2.5rem}.k-search-types>.k-button .k-button-text{opacity:1;font-weight:500}.k-search-input input{background:0 0;flex-grow:1;font:inherit;padding:.75rem;border:0;height:2.5rem}.k-search-close{width:3rem;line-height:1}.k-search-close .k-icon-loader{animation:Spin 2s linear infinite}.k-search input:focus{outline:0}.k-search-results{padding:.5rem 1rem 1rem;background:var(--color-light)}.k-search .k-item:not(:last-child){margin-bottom:.25rem}.k-search .k-item[data-selected=true]{outline:2px solid var(--color-focus)}.k-search .k-item-info,.k-search-empty{font-size:var(--text-xs)}.k-search-empty{text-align:center;color:var(--color-gray-600)}.k-tag{position:relative;font-size:var(--text-sm);line-height:1;cursor:pointer;background-color:var(--color-gray-900);color:var(--color-light);border-radius:var(--rounded-sm);display:flex;align-items:center;justify-content:space-between;-webkit-user-select:none;-moz-user-select:none;user-select:none}.k-tag:focus{outline:0;background-color:var(--color-focus);color:#fff}.k-tag-text{padding:.3rem .75rem .375rem;line-height:var(--leading-tight)}[dir=ltr] .k-tag-toggle{border-left:1px solid rgba(255,255,255,.15)}[dir=rtl] .k-tag-toggle{border-right:1px solid rgba(255,255,255,.15)}.k-tag-toggle{color:#ffffffb3;width:1.75rem;height:100%;display:flex;flex-shrink:0;align-items:center;justify-content:center}.k-tag-toggle:hover{background:rgba(255,255,255,.2);color:#fff}[data-disabled=true] .k-tag{background-color:var(--color-gray-600)}[data-disabled=true] .k-tag .k-tag-toggle{display:none}.k-topbar{--bg:var(--color-gray-900);position:relative;color:var(--color-white);flex-shrink:0;height:2.5rem;line-height:1;background:var(--bg)}.k-topbar-wrapper{position:relative;display:flex;align-items:center;margin-left:-.75rem;margin-right:-.75rem}[dir=ltr] .k-topbar-wrapper:after{left:100%}[dir=rtl] .k-topbar-wrapper:after{right:100%}.k-topbar-wrapper:after{position:absolute;content:"";height:2.5rem;background:var(--bg);width:3rem}.k-topbar-menu{flex-shrink:0}.k-topbar-menu ul{padding:.5rem 0}.k-topbar .k-button[data-theme]{color:var(--theme-light)}.k-topbar .k-button-text{opacity:1}.k-topbar-menu-button{display:flex;align-items:center}.k-topbar-menu .k-link[aria-current]{color:var(--color-focus);font-weight:500}.k-topbar-button{padding:.75rem;line-height:1;font-size:var(--text-sm)}.k-topbar-button .k-button-text{display:flex}[dir=ltr] .k-topbar-view-button{padding-right:0}[dir=rtl] .k-topbar-view-button{padding-left:0}.k-topbar-view-button{flex-shrink:0;display:flex;align-items:center}[dir=ltr] .k-topbar-view-button .k-icon{margin-right:.5rem}[dir=rtl] .k-topbar-view-button .k-icon{margin-left:.5rem}[dir=ltr] .k-topbar-signals{right:0}[dir=rtl] .k-topbar-signals{left:0}.k-topbar-signals{position:absolute;top:0;background:var(--bg);height:2.5rem;display:flex;align-items:center}.k-topbar-signals:before{position:absolute;content:"";top:-.5rem;bottom:0;width:.5rem;background:-webkit-linear-gradient(inline-start,rgba(17,17,17,0),#111)}.k-topbar-signals .k-button{line-height:1}.k-topbar-notification{font-weight:var(--font-bold);line-height:1;display:flex}@media screen and (max-width:30em){.k-topbar .k-button[data-theme=negative] .k-button-text{display:none}}.k-section,.k-sections{padding-bottom:3rem}.k-section-header{position:relative;display:flex;align-items:baseline;z-index:1}.k-section-header .k-headline{line-height:1.25rem;padding-bottom:.75rem;min-height:2rem}[dir=ltr] .k-section-header .k-button-group{right:0}[dir=rtl] .k-section-header .k-button-group{left:0}.k-section-header .k-button-group{position:absolute;top:-.875rem}.k-info-section-headline{margin-bottom:.5rem}.k-pages-section[data-processing=true],.k-files-section[data-processing=true]{pointer-events:none}.k-fields-issue-headline{margin-bottom:.5rem}.k-fields-section input[type=submit]{display:none}[data-locked=true] .k-fields-section{opacity:.2;pointer-events:none}.k-user-profile{background:var(--color-white)}.k-user-profile>.k-view{padding-top:3rem;padding-bottom:3rem;display:flex;align-items:center;line-height:0}[dir=ltr] .k-user-profile .k-button-group{margin-left:.75rem}[dir=rtl] .k-user-profile .k-button-group{margin-right:.75rem}.k-user-profile .k-button-group{overflow:hidden}.k-user-profile .k-button-group .k-button{display:block;padding-top:.25rem;padding-bottom:.25rem;overflow:hidden;white-space:nowrap}.k-user-view-image .k-icon,.k-user-view-image .k-image{width:5rem;height:5rem;line-height:0}.k-user-view-image[data-disabled=true]{opacity:1}.k-user-view-image .k-image{display:block}.k-user-view-image .k-button-text{opacity:1}.k-user-name-placeholder{color:var(--color-gray-500);transition:color .3s}.k-header[data-editable=true] .k-user-name-placeholder:hover{color:var(--color-gray-900)}.k-error-view{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center}.k-error-view-content{line-height:1.5em;max-width:25rem;text-align:center}.k-error-view-icon{color:var(--color-negative);display:inline-block}.k-error-view-content p:not(:last-child){margin-bottom:.75rem}.k-installation-view .k-button{display:block;margin-top:1.5rem}.k-installation-view .k-headline{margin-bottom:.75rem}.k-installation-issues{line-height:1.5em;font-size:var(--text-sm)}[dir=ltr] .k-installation-issues li{padding-left:3.5rem}[dir=rtl] .k-installation-issues li{padding-right:3.5rem}.k-installation-issues li{position:relative;padding:1.5rem;background:var(--color-white)}[dir=ltr] .k-installation-issues .k-icon{left:1.5rem}[dir=rtl] .k-installation-issues .k-icon{right:1.5rem}.k-installation-issues .k-icon{position:absolute;top:calc(1.5rem + 2px)}.k-installation-issues .k-icon svg *{fill:var(--color-negative)}.k-installation-issues li:not(:last-child){margin-bottom:2px}.k-installation-issues li code{font:inherit;color:var(--color-negative)}[dir=ltr] .k-installation-view .k-button[type=submit]{margin-left:-1rem}[dir=rtl] .k-installation-view .k-button[type=submit]{margin-right:-1rem}.k-installation-view .k-button[type=submit]{padding:1rem}.k-languages-view .k-header{margin-bottom:1.5rem}.k-languages-view-section-header{margin-bottom:.5rem}.k-languages-view-section{margin-bottom:3rem}.k-login-fields{position:relative}[dir=ltr] .k-login-toggler{right:0}[dir=rtl] .k-login-toggler{left:0}.k-login-toggler{position:absolute;top:0;z-index:1;text-decoration:underline;font-size:.875rem}.k-login-form label abbr{visibility:hidden}.k-login-buttons{display:flex;align-items:center;justify-content:flex-end;padding:1.5rem 0}[dir=ltr] .k-login-button{margin-right:-1rem}[dir=rtl] .k-login-button{margin-left:-1rem}.k-login-button{padding:.5rem 1rem;font-weight:500;transition:opacity .3s}.k-login-button span{opacity:1}.k-login-button[disabled]{opacity:.25}.k-login-back-button,.k-login-checkbox{display:flex;align-items:center;flex-grow:1}[dir=ltr] .k-login-back-button{margin-left:-1rem}[dir=rtl] .k-login-back-button{margin-right:-1rem}.k-login-checkbox{padding:.5rem 0;font-size:var(--text-sm);cursor:pointer}.k-login-checkbox .k-checkbox-text{opacity:.75;transition:opacity .3s}.k-login-checkbox:focus span,.k-login-checkbox:hover span{opacity:1}.k-password-reset-view .k-user-info{height:38px;margin-bottom:2.25rem;padding:.5rem;background:var(--color-white);border-radius:var(--rounded-xs);box-shadow:var(--shadow)}.k-system-view .k-header{margin-bottom:1.5rem}.k-system-view-section-header{margin-bottom:.5rem}.k-system-view-section{margin-bottom:3rem}.k-system-info-box{display:grid;grid-gap:1px;font-size:var(--text-sm)}@media screen and (min-width:45rem){.k-system-info-box{grid-template-columns:repeat(var(--columns),1fr)}}.k-system-info-box li,.k-system-plugins td,.k-system-plugins th{padding:.75rem;background:var(--color-white)}.k-system-info-box dt{font-size:var(--text-sm);color:var(--color-gray-600);margin-bottom:.25rem}.k-system-warning{color:var(--color-negative);font-weight:var(--font-bold);display:inline-flex}.k-system-warning .k-button-text{opacity:1}.k-system-plugins{width:100%;font-variant-numeric:tabular-nums;table-layout:fixed;border-spacing:1px}.k-system-plugins td,.k-system-plugins th{text-align:left;font-weight:var(--font-normal);font-size:var(--text-sm);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-system-plugins .desk{display:none}@media screen and (min-width:45rem){.k-system-plugins .desk{display:table-cell}}.k-system-plugins th{color:var(--color-gray-600)}.k-block-type-code-editor{position:relative;font-size:var(--text-sm);line-height:1.5em;background:#000;border-radius:var(--rounded);padding:.5rem .75rem 3rem;color:#fff;font-family:var(--font-mono)}.k-block-type-code-editor .k-editor{white-space:pre-wrap;line-height:1.75em}[dir=ltr] .k-block-type-code-editor-language{right:0}[dir=ltr] .k-block-type-code-editor-language .k-icon,[dir=rtl] .k-block-type-code-editor-language{left:0}.k-block-type-code-editor-language{font-size:var(--text-sm);position:absolute;bottom:0}[dir=rtl] .k-block-type-code-editor-language .k-icon{right:0}.k-block-type-code-editor-language .k-icon{position:absolute;top:0;height:1.5rem;display:flex;width:2rem;z-index:0}.k-block-type-code-editor-language .k-select-input{position:relative;padding:.325rem .75rem .5rem 2rem;z-index:1;font-size:var(--text-xs)}.k-block-type-default .k-block-title{line-height:1.5em}.k-block-type-gallery ul{display:grid;grid-gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(6rem,1fr));line-height:0;align-items:center;justify-content:center;cursor:pointer}.k-block-type-gallery li:empty{padding-bottom:100%;background:var(--color-background)}.k-block-type-gallery li{display:flex;position:relative;align-items:center;justify-content:center}.k-block-type-gallery li img{flex-grow:1;max-width:100%}.k-block-type-heading-input{line-height:1.25em;font-weight:var(--font-bold)}.k-block-type-heading-input[data-level=h1]{font-size:var(--text-3xl);line-height:1.125em}.k-block-type-heading-input[data-level=h2]{font-size:var(--text-2xl)}.k-block-type-heading-input[data-level=h3]{font-size:var(--text-xl)}.k-block-type-heading-input[data-level=h4]{font-size:var(--text-lg)}.k-block-type-heading-input[data-level=h5]{line-height:1.5em;font-size:var(--text-base)}.k-block-type-heading-input[data-level=h6]{line-height:1.5em;font-size:var(--text-sm)}.k-block-type-heading-input .ProseMirror strong{font-weight:700}.k-block-type-image .k-block-figure-container{display:block;text-align:center;line-height:0}.k-block-type-image-auto{max-width:100%;max-height:30rem}.k-block-type-line hr{margin-top:.75rem;margin-bottom:.75rem;border:0;border-top:2px solid var(--color-gray-400)}.k-block-type-markdown-input{position:relative;font-size:var(--text-sm);line-height:1.5em;background:var(--color-background);border-radius:var(--rounded);padding:.5rem .5rem 0;font-family:var(--font-mono)}[dir=ltr] .k-block-type-quote-editor{padding-left:1rem}[dir=rtl] .k-block-type-quote-editor{padding-right:1rem}[dir=ltr] .k-block-type-quote-editor{border-left:2px solid var(--color-black)}[dir=rtl] .k-block-type-quote-editor{border-right:2px solid var(--color-black)}.k-block-type-quote-text{font-size:var(--text-xl);margin-bottom:.25rem;line-height:1.25em}.k-block-type-quote-citation{font-style:italic;font-size:var(--text-sm);color:var(--color-gray-600)}.k-block-type-table-preview{cursor:pointer;width:100%;border:1px solid var(--color-gray-300);border-spacing:0;border-radius:var(--rounded-sm);overflow:hidden;table-layout:fixed;--item-height:38px}[dir=ltr] .k-block-type-table-preview td,[dir=ltr] .k-block-type-table-preview th{text-align:left}[dir=rtl] .k-block-type-table-preview td,[dir=rtl] .k-block-type-table-preview th{text-align:right}.k-block-type-table-preview td{line-height:1.5em;font-size:var(--text-sm);height:var(--item-height);padding:0 .75rem}.k-block-type-table-preview th{line-height:1.5em;padding:.5rem .75rem}.k-block-type-table-preview td [class$=-field-preview],.k-block-type-table-preview td>*{padding:0}.k-block-type-table-preview th,.k-block-type-table-preview tr:not(:last-child) td{border-bottom:1px solid var(--color-gray-300)}.k-block-type-table-preview th{background:var(--color-gray-100);font-family:var(--font-mono);font-size:var(--text-xs)}.k-block-type-table-preview-empty{color:var(--color-gray-600);font-size:var(--text-sm)}.k-block-type-table-preview [data-align]{text-align:var(--align)}.k-block-type-text-input{font-size:var(--text-base);line-height:1.5em;height:100%}.k-block-container-type-text,.k-block-type-text,.k-block-type-text .k-writer .ProseMirror{height:100%}.k-block-container{position:relative;padding:.75rem;background:var(--color-white)}.k-block-container:not(:last-of-type){border-bottom:1px dashed rgba(0,0,0,.1)}.k-block-container:focus{outline:0}.k-block-container[data-batched=true],.k-block-container[data-selected=true]{z-index:2;border-bottom-color:transparent}.k-block-container[data-batched=true]:after{position:absolute;top:0;right:0;bottom:0;left:0;content:"";background:rgba(238,242,246,.375);mix-blend-mode:multiply;border:1px solid var(--color-focus)}.k-block-container[data-selected=true]{box-shadow:var(--color-focus) 0 0 0 1px,var(--color-focus-outline) 0 0 0 3px}[dir=ltr] .k-block-container .k-block-options{right:.75rem}[dir=rtl] .k-block-container .k-block-options{left:.75rem}.k-block-container .k-block-options{display:none;position:absolute;top:0;margin-top:calc(-1.75rem + 2px)}.k-block-container[data-last-in-batch=true]>.k-block-options,.k-block-container[data-selected=true]>.k-block-options{display:flex}.k-block-container[data-hidden=true] .k-block{opacity:.25}.k-drawer-options .k-button[data-disabled=true]{vertical-align:middle;display:inline-grid}[data-disabled=true] .k-block-container{background:var(--color-background)}.k-block-importer.k-dialog{background:#313740;color:var(--color-white)}.k-block-importer .k-dialog-body{padding:0}.k-block-importer label{display:block;padding:var(--spacing-6) var(--spacing-6)0;color:var(--color-gray-400)}.k-block-importer label kbd{background:rgba(0,0,0,.5);font-family:var(--font-mono);letter-spacing:.1em;padding:.25rem;border-radius:var(--rounded);margin:0 .25rem}.k-block-importer textarea{width:100%;height:20rem;background:0 0;font:inherit;color:var(--color-white);border:0;padding:var(--spacing-6);resize:none}.k-block-importer textarea:focus{outline:0}.k-blocks{background:var(--color-white);box-shadow:var(--shadow)}[data-disabled=true] .k-blocks{background:var(--color-background)}.k-blocks[data-multi-select-key=true] .k-block-container>*{pointer-events:none}.k-blocks[data-empty=true]{padding:0;background:0 0;box-shadow:none}.k-blocks .k-sortable-ghost{outline:2px solid var(--color-focus);box-shadow:#11111140 0 5px 10px;cursor:grabbing;cursor:-webkit-grabbing}.k-blocks-list>.k-blocks-empty{display:flex;align-items:center}.k-blocks-list>.k-blocks-empty:not(:only-child){display:none}.k-block-figure{cursor:pointer}.k-block-figure iframe{border:0;pointer-events:none;background:var(--color-black)}.k-block-figure figcaption{padding-top:.5rem;color:var(--color-gray-600);font-size:var(--text-sm);text-align:center}.k-block-figure-empty.k-button{display:flex;width:100%;height:6rem;border-radius:var(--rounded-sm);align-items:center;justify-content:center;color:var(--color-gray-600);background:var(--color-background)}.k-block-options{display:flex;align-items:center;background:var(--color-white);z-index:var(--z-dropdown);box-shadow:#0000001a -2px 0 5px,var(--shadow),var(--shadow-xl);color:var(--color-black);border-radius:var(--rounded)}[dir=ltr] .k-block-options-button{border-right:1px solid var(--color-background)}[dir=rtl] .k-block-options-button{border-left:1px solid var(--color-background)}.k-block-options-button{--block-options-button-size:30px;width:var(--block-options-button-size);height:var(--block-options-button-size);line-height:1;display:inline-flex;align-items:center;justify-content:center}[dir=ltr] .k-block-options-button:first-child{border-top-left-radius:var(--rounded)}[dir=rtl] .k-block-options-button:first-child{border-top-right-radius:var(--rounded)}[dir=ltr] .k-block-options-button:first-child{border-bottom-left-radius:var(--rounded)}[dir=rtl] .k-block-options-button:first-child{border-bottom-right-radius:var(--rounded)}[dir=ltr] .k-block-options-button:last-child{border-top-right-radius:var(--rounded)}[dir=rtl] .k-block-options-button:last-child{border-top-left-radius:var(--rounded)}[dir=ltr] .k-block-options-button:last-child{border-bottom-right-radius:var(--rounded)}[dir=rtl] .k-block-options-button:last-child{border-bottom-left-radius:var(--rounded)}[dir=ltr] .k-block-options-button:last-of-type{border-right:0}[dir=rtl] .k-block-options-button:last-of-type{border-left:0}.k-block-options-button[aria-current]{color:var(--color-focus)}.k-block-options-button:hover{background:var(--color-gray-100)}.k-block-options .k-dropdown-content{margin-top:.5rem}.k-block-selector.k-dialog{background:var(--color-dark);color:var(--color-white)}.k-block-selector .k-headline{margin-bottom:1rem}.k-block-selector details:not(:last-of-type){margin-bottom:1.5rem}.k-block-selector summary{font-size:var(--text-xs);cursor:pointer;color:var(--color-gray-400)}.k-block-selector details:only-of-type summary{pointer-events:none}.k-block-selector summary:focus{outline:0}.k-block-selector summary:focus-visible{color:var(--color-green-400)}.k-block-types{display:grid;grid-gap:2px;margin-top:.75rem;grid-template-columns:repeat(1,1fr)}[dir=ltr] .k-block-types .k-button{text-align:left}[dir=rtl] .k-block-types .k-button{text-align:right}.k-block-types .k-button{display:flex;align-items:flex-start;background:rgba(0,0,0,.5);width:100%;padding:0 .75rem 0 0;line-height:1.5em}.k-block-types .k-button:focus{outline:2px solid var(--color-green-300)}.k-block-types .k-button .k-button-text{padding:.5rem 0 .5rem .5rem}.k-block-types .k-button .k-icon{width:38px;height:38px}.k-clipboard-hint{padding-top:1.5rem;font-size:var(--text-xs);color:var(--color-gray-400)}.k-clipboard-hint kbd{background:rgba(0,0,0,.5);font-family:var(--font-mono);letter-spacing:.1em;padding:.25rem;border-radius:var(--rounded);margin:0 .25rem}[dir=ltr] .k-block-title{padding-right:.75rem}[dir=rtl] .k-block-title{padding-left:.75rem}.k-block-title{display:flex;align-items:center;min-width:0;font-size:var(--text-sm);line-height:1}[dir=ltr] .k-block-icon{margin-right:.5rem}[dir=rtl] .k-block-icon{margin-left:.5rem}.k-block-icon{width:1rem;color:var(--color-gray-500)}[dir=ltr] .k-block-name{margin-right:.5rem}[dir=rtl] .k-block-name{margin-left:.5rem}.k-block-label{color:var(--color-gray-600);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[data-align=left]{--align:start}[data-align=center]{--align:center}[data-align=right]{--align:end}[data-invalid=true]{border:1px solid var(--color-negative-outline);box-shadow:var(--color-negative-outline) 0 0 3px 2px}[data-invalid=true]:focus-within{border:var(--field-input-invalid-focus-border)!important;box-shadow:var(--color-negative-outline) 0 0 0 2px!important}[data-tabbed=true]{box-shadow:var(--shadow-outline)}[data-theme=positive],[data-theme=success]{--theme:var(--color-positive);--theme-light:var(--color-positive-light);--theme-bg:var(--color-green-300)}[data-theme=error],[data-theme=negative]{--theme:var(--color-negative);--theme-light:var(--color-negative-light);--theme-bg:var(--color-red-300)}[data-theme=notice]{--theme:var(--color-notice);--theme-light:var(--color-notice-light);--theme-bg:var(--color-orange-300)}[data-theme=info]{--theme:var(--color-focus);--theme-light:var(--color-focus-light);--theme-bg:var(--color-blue-200)}.scroll-x,.scroll-x-auto,.scroll-y,.scroll-y-auto{-webkit-overflow-scrolling:touch;transform:translate(0)}.scroll-x,.scroll-x-auto{overflow-x:scroll;overflow-y:hidden}.scroll-x-auto{overflow-x:auto}.scroll-y,.scroll-y-auto{overflow-x:hidden;overflow-y:scroll}.scroll-y-auto{overflow-y:auto}.k-offscreen,.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0} +:root{--color-backdrop:rgba(0, 0, 0, .6);--color-black:#000;--color-dark:#313740;--color-light:var(--color-gray-200);--color-white:#fff;--color-gray-100:#f7f7f7;--color-gray-200:#efefef;--color-gray-300:#ddd;--color-gray-400:#ccc;--color-gray-500:#999;--color-gray-600:#777;--color-gray-700:#555;--color-gray-800:#333;--color-gray-900:#111;--color-gray:var(--color-gray-600);--color-red-200:#edc1c1;--color-red-300:#e3a0a0;--color-red-400:#d16464;--color-red-600:#c82829;--color-red:var(--color-red-600);--color-orange-200:#f2d4bf;--color-orange-300:#ebbe9e;--color-orange-400:#de935f;--color-orange-600:#f4861f;--color-orange:var(--color-orange-600);--color-yellow-200:#f9e8c7;--color-yellow-300:#f7e2b8;--color-yellow-400:#f0c674;--color-yellow-600:#cca000;--color-yellow:var(--color-yellow-600);--color-green-200:#dce5c2;--color-green-300:#c6d49d;--color-green-400:#a7bd68;--color-green-600:#5d800d;--color-green:var(--color-green-600);--color-aqua-200:#d0e5e2;--color-aqua-300:#bbd9d5;--color-aqua-400:#8abeb7;--color-aqua-600:#398e93;--color-aqua:var(--color-aqua-600);--color-blue-200:#cbd7e5;--color-blue-300:#b1c2d8;--color-blue-400:#7e9abf;--color-blue-600:#4271ae;--color-blue:var(--color-blue-600);--color-purple-200:#e0d4e4;--color-purple-300:#d4c3d9;--color-purple-400:#b294bb;--color-purple-600:#9c48b9;--color-purple:var(--color-purple-600);--container:80rem;--font-sans:-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";--font-mono:"SFMono-Regular", Consolas, Liberation Mono, Menlo, Courier, monospace;--font-normal:400;--font-bold:600;--leading-none:1;--leading-tight:1.25;--leading-snug:1.375;--leading-normal:1.5;--leading-relaxed:1.625;--leading-loose:2;--rounded-xs:1px;--rounded-sm:.125rem;--rounded:.25rem;--rounded-md:.375rem;--shadow:0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06);--shadow-md:0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06);--shadow-lg:0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05);--shadow-xl:0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04);--shadow-outline:currentColor 0 0 0 2px;--shadow-inset:inset 0 2px 4px 0 rgba(0, 0, 0, .06);--spacing-0:0;--spacing-px:1px;--spacing-2px:2px;--spacing-1:.25rem;--spacing-2:.5rem;--spacing-3:.75rem;--spacing-4:1rem;--spacing-5:1.25rem;--spacing-6:1.5rem;--spacing-8:2rem;--spacing-10:2.5rem;--spacing-12:3rem;--spacing-16:4rem;--spacing-20:5rem;--spacing-24:6rem;--spacing-36:9rem;--text-xs:.75rem;--text-sm:.875rem;--text-base:1rem;--text-lg:1.125rem;--text-xl:1.25rem;--text-2xl:1.5rem;--text-3xl:1.75rem;--text-4xl:2.5rem;--text-5xl:3rem;--text-6xl:4rem;--color-background:var(--color-light);--color-border:var(--color-gray-400);--color-focus:var(--color-blue-600);--color-focus-light:var(--color-blue-400);--color-focus-outline:rgba(113, 143, 183, .25);--color-negative:var(--color-red-600);--color-negative-light:var(--color-red-400);--color-negative-outline:rgba(212, 110, 110, .25);--color-notice:var(--color-orange-600);--color-notice-light:var(--color-orange-400);--color-positive:var(--color-green-600);--color-positive-light:var(--color-green-400);--color-positive-outline:rgba(128, 149, 65, .25);--color-text:var(--color-gray-900);--color-text-light:var(--color-gray-600);--z-offline:1200;--z-fatal:1100;--z-loader:1000;--z-notification:900;--z-dialog:800;--z-navigation:700;--z-dropdown:600;--z-drawer:500;--z-dropzone:400;--z-toolbar:300;--z-content:200;--z-background:100;--bg-pattern:repeating-conic-gradient( rgba(0, 0, 0, 0) 0% 25%, rgba(0, 0, 0, .2) 0% 50% ) 50% / 20px 20px;--shadow-sticky:rgba(0, 0, 0, .05) 0 2px 5px;--shadow-dropdown:var(--shadow-lg);--shadow-item:var(--shadow);--field-input-padding:.5rem;--field-input-height:2.25rem;--field-input-line-height:1.25rem;--field-input-font-size:var(--text-base);--field-input-color-before:var(--color-gray-700);--field-input-color-after:var(--color-gray-700);--field-input-border:1px solid var(--color-border);--field-input-focus-border:1px solid var(--color-focus);--field-input-focus-outline:2px solid var(--color-focus-outline);--field-input-invalid-border:1px solid var(--color-negative-outline);--field-input-invalid-outline:0;--field-input-invalid-focus-border:1px solid var(--color-negative);--field-input-invalid-focus-outline:2px solid var(--color-negative-outline);--field-input-background:var(--color-white);--field-input-disabled-color:var(--color-gray-500);--field-input-disabled-background:var(--color-white);--field-input-disabled-border:1px solid var(--color-gray-300);--font-family-sans:var(--font-sans);--font-family-mono:var(--font-mono);--font-size-tiny:var(--text-xs);--font-size-small:var(--text-sm);--font-size-medium:var(--text-base);--font-size-large:var(--text-xl);--font-size-huge:var(--text-2xl);--font-size-monster:var(--text-3xl);--box-shadow-dropdown:var(--shadow-dropdown);--box-shadow-item:var(--shadow);--box-shadow-focus:var(--shadow-xl)}*,:after,:before{margin:0;padding:0;box-sizing:border-box}noscript{padding:1.5rem;display:flex;align-items:center;justify-content:center;height:100vh;text-align:center}html{font-family:var(--font-sans);background:var(--color-background)}body,html{color:var(--color-gray-900);height:100%;overflow:hidden}a{color:inherit;text-decoration:none}li{list-style:none}b,strong{font-weight:var(--font-bold)}@keyframes LoadingCursor{to{cursor:progress}}@keyframes Spin{to{transform:rotate(360deg)}}.k-dialog{position:relative;background:var(--color-background);width:100%;box-shadow:var(--shadow-lg);border-radius:var(--rounded-md);line-height:1;max-height:calc(100vh - 3rem);margin:1.5rem;display:flex;flex-direction:column}@media screen and (min-width:20rem){.k-dialog[data-size=small]{width:20rem}}@media screen and (min-width:22rem){.k-dialog[data-size=default]{width:22rem}}@media screen and (min-width:30rem){.k-dialog[data-size=medium]{width:30rem}}@media screen and (min-width:40rem){.k-dialog[data-size=large]{width:40rem}}[dir=ltr] .k-dialog-notification,[dir=rtl] .k-dialog-notification{border-top-right-radius:var(--rounded);border-top-left-radius:var(--rounded)}.k-dialog-notification{padding:.75rem 1.5rem;background:var(--color-gray-900);width:100%;margin-top:-1px;line-height:1.25rem;color:var(--color-white);display:flex;flex-shrink:0;align-items:center}.k-dialog-notification[data-theme]{background:var(--theme-light);color:var(--color-black)}.k-dialog-notification p{flex-grow:1;word-wrap:break-word;overflow:hidden}[dir=ltr] .k-dialog-notification .k-button{margin-left:1rem}[dir=rtl] .k-dialog-notification .k-button{margin-right:1rem}.k-dialog-notification .k-button{display:flex}.k-dialog-body{padding:1.5rem}.k-dialog-body .k-fieldset{padding-bottom:.5rem}.k-dialog-footer{padding:0;border-top:1px solid var(--color-gray-300);line-height:1;flex-shrink:0}.k-dialog-footer .k-button-group{display:flex;margin:0;justify-content:space-between}.k-dialog-footer .k-button-group .k-button{padding:.75rem 1rem;line-height:1.25rem}[dir=ltr] .k-dialog-footer .k-button-group .k-button:first-child{text-align:left}[dir=rtl] .k-dialog-footer .k-button-group .k-button:first-child{text-align:right}[dir=ltr] .k-dialog-footer .k-button-group .k-button:first-child{padding-left:1.5rem}[dir=rtl] .k-dialog-footer .k-button-group .k-button:first-child{padding-right:1.5rem}[dir=ltr] .k-dialog-footer .k-button-group .k-button:last-child{text-align:right}[dir=rtl] .k-dialog-footer .k-button-group .k-button:last-child{text-align:left}[dir=ltr] .k-dialog-footer .k-button-group .k-button:last-child{padding-right:1.5rem}[dir=rtl] .k-dialog-footer .k-button-group .k-button:last-child{padding-left:1.5rem}.k-dialog .k-pagination{margin-bottom:-1.5rem;display:flex;justify-content:center;align-items:center}.k-dialog-search{margin-bottom:.75rem}.k-dialog-search.k-input{background:rgba(0,0,0,.075);padding:0 1rem;height:36px;border-radius:var(--rounded)}.k-error-details{background:var(--color-white);display:block;overflow:auto;padding:1rem;font-size:var(--text-sm);line-height:1.25em;margin-top:.75rem}.k-error-details dt{color:var(--color-negative-light);margin-bottom:.25rem}.k-error-details dd{overflow:hidden;overflow-wrap:break-word;text-overflow:ellipsis}.k-error-details dd:not(:last-of-type){margin-bottom:1.5em}.k-error-details li:not(:last-child){border-bottom:1px solid var(--color-background);padding-bottom:.25rem;margin-bottom:.25rem}.k-files-dialog .k-list-item{cursor:pointer}[dir=ltr] .k-pages-dialog-navbar{padding-right:38px}[dir=rtl] .k-pages-dialog-navbar{padding-left:38px}.k-pages-dialog-navbar{display:flex;align-items:center;justify-content:center;margin-bottom:.5rem}.k-pages-dialog-navbar .k-button{width:38px}.k-pages-dialog-navbar .k-button[disabled]{opacity:0}.k-pages-dialog-navbar .k-headline{flex-grow:1;text-align:center}.k-pages-dialog .k-list-item{cursor:pointer}.k-pages-dialog .k-list-item .k-button[data-theme=disabled],.k-pages-dialog .k-list-item .k-button[disabled]{opacity:.25}.k-pages-dialog .k-list-item .k-button[data-theme=disabled]:hover{opacity:1}.k-users-dialog .k-list-item{cursor:pointer}.k-drawer{--drawer-header-height:2.5rem;--drawer-header-padding:1.5rem;position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--z-toolbar);display:flex;align-items:stretch;justify-content:flex-end;background:rgba(0,0,0,.2)}.k-drawer-box{position:relative;flex-basis:50rem;display:flex;flex-direction:column;background:var(--color-background);box-shadow:var(--shadow-xl)}[dir=ltr] .k-drawer-header{padding-left:var(--drawer-header-padding)}[dir=rtl] .k-drawer-header{padding-right:var(--drawer-header-padding)}.k-drawer-header{flex-shrink:0;height:var(--drawer-header-height);display:flex;align-items:center;line-height:1;justify-content:space-between;background:var(--color-white);font-size:var(--text-sm)}.k-drawer-title{padding:0 .75rem}[dir=ltr] .k-drawer-breadcrumb,[dir=ltr] .k-drawer-title{margin-left:-.75rem}[dir=rtl] .k-drawer-breadcrumb,[dir=rtl] .k-drawer-title{margin-right:-.75rem}.k-drawer-breadcrumb,.k-drawer-title{display:flex;flex-grow:1;align-items:center;min-width:0;font-size:var(--text-sm);font-weight:var(--font-normal)}[dir=ltr] .k-drawer-breadcrumb li:not(:last-child) .k-button:after{right:-.75rem}[dir=rtl] .k-drawer-breadcrumb li:not(:last-child) .k-button:after{left:-.75rem}.k-drawer-breadcrumb li:not(:last-child) .k-button:after{position:absolute;width:1.5rem;display:inline-flex;justify-content:center;align-items:center;content:"\203a";color:var(--color-gray-500);height:var(--drawer-header-height)}[dir=ltr] .k-drawer-breadcrumb .k-icon,[dir=ltr] .k-drawer-title .k-icon{margin-right:.5rem}[dir=rtl] .k-drawer-breadcrumb .k-icon,[dir=rtl] .k-drawer-title .k-icon{margin-left:.5rem}.k-drawer-breadcrumb .k-icon,.k-drawer-title .k-icon{width:1rem;color:var(--color-gray-500)}.k-drawer-breadcrumb .k-button{display:inline-flex;align-items:center;height:var(--drawer-header-height);padding-left:.75rem;padding-right:.75rem}.k-drawer-breadcrumb .k-button-text{opacity:1}[dir=ltr] .k-drawer-breadcrumb .k-button .k-button-icon~.k-button-text{padding-left:0}[dir=rtl] .k-drawer-breadcrumb .k-button .k-button-icon~.k-button-text{padding-right:0}[dir=ltr] .k-drawer-tabs{margin-right:.75rem}[dir=rtl] .k-drawer-tabs{margin-left:.75rem}.k-drawer-tabs{display:flex;align-items:center;line-height:1}.k-drawer-tab.k-button{height:var(--drawer-header-height);padding-left:.75rem;padding-right:.75rem;display:flex;align-items:center;font-size:var(--text-xs)}.k-drawer-tab.k-button[aria-current]:after{position:absolute;bottom:-1px;left:.75rem;right:.75rem;content:"";background:var(--color-black);height:2px}[dir=ltr] .k-drawer-options{padding-right:.75rem}[dir=rtl] .k-drawer-options{padding-left:.75rem}.k-drawer-option.k-button{width:var(--drawer-header-height);height:var(--drawer-header-height);color:var(--color-gray-500);line-height:1}.k-drawer-option.k-button:focus,.k-drawer-option.k-button:hover{color:var(--color-black)}.k-drawer-body{padding:1.5rem;flex-grow:1;background:var(--color-background)}.k-drawer[data-nested=true]{background:0 0}.k-drawer-body .k-table th,.k-drawer-body .k-textarea-input:focus-within .k-toolbar{top:-1.5rem}.k-calendar-input{--cell-padding:.25rem .5rem;padding:.5rem;color:var(--color-light);border-radius:var(--rounded-xs)}.k-calendar-table{table-layout:fixed;width:100%;min-width:15rem;padding-top:.5rem}.k-calendar-input>nav{display:flex;direction:ltr}.k-calendar-input>nav .k-button{padding:.5rem}.k-calendar-selects{flex-grow:1;display:flex;align-items:center;justify-content:center}[dir=ltr] .k-calendar-selects{direction:ltr}[dir=rtl] .k-calendar-selects{direction:rtl}.k-calendar-selects .k-select-input{padding:0 .5rem;font-weight:var(--font-normal);font-size:var(--text-sm)}.k-calendar-selects .k-select-input:focus-within{color:var(--color-focus-light)!important}.k-calendar-input th{padding:.5rem 0;color:var(--color-gray-500);font-size:var(--text-xs);font-weight:400;text-align:center}.k-calendar-day .k-button{width:2rem;height:2rem;margin-left:auto;margin-right:auto;color:var(--color-white);line-height:1.75rem;display:flex;justify-content:center;border-radius:50%;border:2px solid transparent}.k-calendar-day .k-button .k-button-text{opacity:1}.k-calendar-table .k-button:hover{color:var(--color-white)}.k-calendar-day:hover .k-button:not([data-disabled=true]){border-color:#ffffff40}.k-calendar-day[aria-current=date] .k-button{text-decoration:underline}.k-calendar-day[aria-selected=date] .k-button{border-color:currentColor;font-weight:600;color:var(--color-focus-light)}.k-calendar-today{text-align:center;padding-top:.5rem}.k-calendar-today .k-button{font-size:var(--text-xs);padding:1rem;text-decoration:underline}.k-calendar-today .k-button-text{opacity:1;vertical-align:baseline}.k-counter{font-size:var(--text-xs);color:var(--color-gray-900);font-weight:var(--font-bold)}.k-counter[data-invalid=true]{box-shadow:none;border:0;color:var(--color-negative)}[dir=ltr] .k-counter-rules{padding-left:.5rem}[dir=rtl] .k-counter-rules{padding-right:.5rem}.k-counter-rules{color:var(--color-gray-600);font-weight:var(--font-normal)}.k-form-submitter{display:none}.k-form-buttons[data-theme]{background:var(--theme-light)}.k-form-buttons .k-view{display:flex;justify-content:space-between;align-items:center}.k-form-button.k-button{font-weight:500;white-space:nowrap;line-height:1;height:2.5rem;display:flex;padding:0 1rem;align-items:center}[dir=ltr] .k-form-button:first-child{margin-left:-1rem}[dir=rtl] .k-form-button:first-child{margin-right:-1rem}[dir=ltr] .k-form-button:last-child{margin-right:-1rem}[dir=rtl] .k-form-button:last-child{margin-left:-1rem}[dir=ltr] .k-form-lock-info{margin-right:3rem}[dir=rtl] .k-form-lock-info{margin-left:3rem}.k-form-lock-info{display:flex;font-size:var(--text-sm);align-items:center;line-height:1.5em;padding:.625rem 0}[dir=ltr] .k-form-lock-info>.k-icon{margin-right:.5rem}[dir=rtl] .k-form-lock-info>.k-icon{margin-left:.5rem}.k-form-lock-buttons{display:flex;flex-shrink:0}.k-form-lock-loader{animation:Spin 4s linear infinite}.k-form-lock-loader .k-icon-loader{display:flex}.k-form-indicator-toggle{color:var(--color-notice-light)}.k-form-indicator-info{font-size:var(--text-sm);font-weight:var(--font-bold);padding:.75rem 1rem .25rem;line-height:1.25em;width:15rem}.k-field-label{font-weight:var(--font-bold);display:block;padding:0 0 .75rem;flex-grow:1;line-height:1.25rem}[dir=ltr] .k-field-label abbr{padding-left:.25rem}[dir=rtl] .k-field-label abbr{padding-right:.25rem}.k-field-label abbr{text-decoration:none;color:var(--color-gray-500)}.k-field-header{position:relative;display:flex;align-items:baseline}[dir=ltr] .k-field-options{right:0}[dir=rtl] .k-field-options{left:0}.k-field-options{position:absolute;top:calc(-.5rem - 1px)}.k-field-options.k-button-group .k-dropdown{height:auto}.k-field-options.k-button-group .k-field-options-button.k-button{padding:.75rem;display:flex}.k-field[data-disabled=true]{cursor:not-allowed}.k-field[data-disabled=true] *{pointer-events:none}.k-field[data-disabled=true] .k-text[data-theme=help] *{pointer-events:initial}.k-field:focus-within>.k-field-header>.k-field-counter{display:block}.k-field-help{padding-top:.5rem}.k-fieldset{border:0}.k-fieldset .k-grid{grid-row-gap:2.25rem}@media screen and (min-width:30em){.k-fieldset .k-grid{grid-column-gap:1.5rem}}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid{grid-template-columns:repeat(1,1fr)}.k-sections>.k-column[data-width="1/3"] .k-fieldset .k-grid .k-column,.k-sections>.k-column[data-width="1/4"] .k-fieldset .k-grid .k-column{grid-column-start:initial}.k-input{display:flex;align-items:center;line-height:1;border:0;outline:0;background:0 0}.k-input-element{flex-grow:1}.k-input-icon{display:flex;justify-content:center;align-items:center;line-height:0}.k-input[data-disabled=true]{pointer-events:none}[data-disabled=true] .k-input-icon{color:var(--color-gray-600)}.k-input[data-theme=field]{line-height:1;border:var(--field-input-border);background:var(--field-input-background);border-radius:var(--rounded)}.k-input[data-theme=field]:focus-within{border:var(--field-input-focus-border);box-shadow:var(--color-focus-outline) 0 0 0 2px}.k-input[data-theme=field][data-disabled=true]{background:var(--color-background)}.k-input[data-theme=field] .k-input-icon{width:var(--field-input-height);align-self:stretch;display:flex;align-items:center;flex-shrink:0}.k-input[data-theme=field] .k-input-after,.k-input[data-theme=field] .k-input-before{align-self:stretch;display:flex;align-items:center;flex-shrink:0;padding:0 var(--field-input-padding)}[dir=ltr] .k-input[data-theme=field] .k-input-before{padding-right:0}[dir=ltr] .k-input[data-theme=field] .k-input-after,[dir=rtl] .k-input[data-theme=field] .k-input-before{padding-left:0}.k-input[data-theme=field] .k-input-before{color:var(--field-input-color-before)}[dir=rtl] .k-input[data-theme=field] .k-input-after{padding-right:0}.k-input[data-theme=field] .k-input-after{color:var(--field-input-color-after)}.k-input[data-theme=field] .k-input-icon>.k-dropdown{width:100%;height:100%}.k-input[data-theme=field] .k-input-icon-button{width:100%;height:100%;display:flex;align-items:center;justify-content:center;flex-shrink:0}.k-input[data-theme=field] .k-number-input,.k-input[data-theme=field] .k-select-input,.k-input[data-theme=field] .k-text-input{padding:var(--field-input-padding);line-height:var(--field-input-line-height)}.k-input[data-theme=field] .k-date-input .k-select-input,.k-input[data-theme=field] .k-time-input .k-select-input{padding-left:0;padding-right:0}[dir=ltr] .k-input[data-theme=field] .k-date-input .k-select-input:first-child,[dir=ltr] .k-input[data-theme=field] .k-time-input .k-select-input:first-child{padding-left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field] .k-date-input .k-select-input:first-child,[dir=rtl] .k-input[data-theme=field] .k-time-input .k-select-input:first-child{padding-right:var(--field-input-padding)}.k-input[data-theme=field] .k-date-input .k-select-input:focus-within,.k-input[data-theme=field] .k-time-input .k-select-input:focus-within{color:var(--color-focus);font-weight:var(--font-bold)}[dir=ltr] .k-input[data-theme=field].k-time-input .k-time-input-meridiem{padding-left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field].k-time-input .k-time-input-meridiem{padding-right:var(--field-input-padding)}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,.k-input[data-theme=field][data-type=checkboxes] .k-radio-input li,.k-input[data-theme=field][data-type=radio] .k-checkboxes-input li,.k-input[data-theme=field][data-type=radio] .k-radio-input li{min-width:0;overflow-wrap:break-word}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-input-before{border-right:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-after,[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-icon,[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-input-before{border-left:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-after,[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-input-element+.k-input-icon{border-right:1px solid var(--color-background)}.k-input[data-theme=field][data-type=checkboxes] .k-input-element{overflow:hidden}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{margin-right:-1px}[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{margin-left:-1px}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px}@media screen and (min-width:65em){.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input{grid-template-columns:repeat(var(--columns),1fr)}}[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li{border-left:1px solid var(--color-background)}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input li,.k-input[data-theme=field][data-type=radio] .k-radio-input li{border-bottom:1px solid var(--color-background)}.k-input[data-theme=field][data-type=checkboxes] .k-checkboxes-input label{display:block;line-height:var(--field-input-line-height);padding:var(--field-input-padding) var(--field-input-padding)}[dir=ltr] .k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon,[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-radio-input label:before{left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-radio-input label:before{right:var(--field-input-padding)}.k-input[data-theme=field][data-type=checkboxes] .k-checkbox-input-icon{top:calc((var(--field-input-height) - var(--field-input-font-size))/2);margin-top:0}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-input-before{border-right:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-after,[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-icon,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-input-before{border-left:1px solid var(--color-background)}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-radio-input li,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-after,[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-input-element+.k-input-icon{border-right:1px solid var(--color-background)}.k-input[data-theme=field][data-type=radio] .k-input-element{overflow:hidden}[dir=ltr] .k-input[data-theme=field][data-type=radio] .k-radio-input{margin-right:-1px}[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-radio-input{margin-left:-1px}.k-input[data-theme=field][data-type=radio] .k-radio-input{display:grid;grid-template-columns:1fr;margin-bottom:-1px}@media screen and (min-width:65em){.k-input[data-theme=field][data-type=radio] .k-radio-input{grid-template-columns:repeat(var(--columns),1fr)}}[dir=rtl] .k-input[data-theme=field][data-type=radio] .k-radio-input li{border-left:1px solid var(--color-background)}.k-input[data-theme=field][data-type=radio] .k-radio-input label{display:block;flex-grow:1;min-height:var(--field-input-height);line-height:var(--field-input-line-height);padding:calc((var(--field-input-height) - var(--field-input-line-height))/2) var(--field-input-padding)}.k-input[data-theme=field][data-type=radio] .k-radio-input label:before{top:calc((var(--field-input-height) - 1rem)/2);margin-top:-1px}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-radio-input-info{display:block;font-size:var(--text-sm);color:var(--color-gray-600);line-height:var(--field-input-line-height);padding-top:calc(var(--field-input-line-height)/10)}.k-input[data-theme=field][data-type=radio] .k-radio-input .k-icon{width:var(--field-input-height);height:var(--field-input-height);display:flex;align-items:center;justify-content:center}.k-input[data-theme=field][data-type=range] .k-range-input{padding:var(--field-input-padding)}.k-input[data-theme=field][data-type=multiselect],.k-input[data-theme=field][data-type=select]{position:relative}[dir=ltr] .k-input[data-theme=field][data-type=select] .k-input-icon{right:0}[dir=rtl] .k-input[data-theme=field][data-type=select] .k-input-icon{left:0}.k-input[data-theme=field][data-type=select] .k-input-icon{position:absolute;top:0;bottom:0}.k-input[data-theme=field][data-type=tags] .k-tags-input{padding:.25rem .25rem 0}[dir=ltr] .k-input[data-theme=field][data-type=tags] .k-tag{margin-right:.25rem}[dir=rtl] .k-input[data-theme=field][data-type=tags] .k-tag{margin-left:.25rem}.k-input[data-theme=field][data-type=tags] .k-tag{margin-bottom:.25rem;height:auto;min-height:1.75rem;font-size:var(--text-sm)}.k-input[data-theme=field][data-type=tags] .k-tags-input input{font-size:var(--text-sm);padding:0 .25rem;height:1.75rem;line-height:1;margin-bottom:.25rem}.k-input[data-theme=field][data-type=tags] .k-tags-input .k-dropdown-content{top:calc(100% + .5rem + 2px)}.k-input[data-theme=field][data-type=tags] .k-tags-input .k-dropdown-content[data-dropup]{top:calc(100% + .5rem + 2px);bottom:initial;margin-bottom:initial}.k-input[data-theme=field][data-type=multiselect] .k-multiselect-input{padding:.25rem 2rem 0 .25rem;min-height:2.25rem}[dir=ltr] .k-input[data-theme=field][data-type=multiselect] .k-tag{margin-right:.25rem}[dir=rtl] .k-input[data-theme=field][data-type=multiselect] .k-tag{margin-left:.25rem}.k-input[data-theme=field][data-type=multiselect] .k-tag{margin-bottom:.25rem;height:1.75rem;font-size:var(--text-sm)}[dir=ltr] .k-input[data-theme=field][data-type=multiselect] .k-input-icon{right:0}[dir=rtl] .k-input[data-theme=field][data-type=multiselect] .k-input-icon{left:0}.k-input[data-theme=field][data-type=multiselect] .k-input-icon{position:absolute;top:0;bottom:0;pointer-events:none}.k-input[data-theme=field][data-type=textarea] .k-textarea-input-native{padding:.25rem var(--field-input-padding);line-height:1.5rem}[dir=ltr] .k-input[data-theme=field][data-type=toggle] .k-input-before{padding-right:calc(var(--field-input-padding)/2)}[dir=rtl] .k-input[data-theme=field][data-type=toggle] .k-input-before{padding-left:calc(var(--field-input-padding)/2)}[dir=ltr] .k-input[data-theme=field][data-type=toggle] .k-toggle-input{padding-left:var(--field-input-padding)}[dir=rtl] .k-input[data-theme=field][data-type=toggle] .k-toggle-input{padding-right:var(--field-input-padding)}.k-input[data-theme=field][data-type=toggle] .k-toggle-input-label{padding:0 var(--field-input-padding)0 .75rem;line-height:var(--field-input-height)}.k-login-code-form .k-user-info{height:38px;margin-bottom:2.25rem;padding:.5rem;background:var(--color-white);border-radius:var(--rounded-xs);box-shadow:var(--shadow)}.k-times{padding:var(--spacing-4) var(--spacing-6);display:grid;line-height:1;grid-template-columns:1fr 1fr;grid-gap:var(--spacing-6)}.k-times .k-icon{width:1rem;margin-bottom:var(--spacing-2)}.k-times-slot .k-button{padding:var(--spacing-1) var(--spacing-3) var(--spacing-1)0;font-variant-numeric:tabular-nums;white-space:nowrap}.k-times .k-times-slot hr{position:relative;opacity:1;margin:var(--spacing-2)0;border:0;height:1px;top:1px;background:var(--color-dark)}[dir=ltr] .k-upload input{left:-3000px}[dir=rtl] .k-upload input{right:-3000px}.k-upload input{position:absolute;top:0}.k-upload-dialog .k-headline{margin-bottom:.75rem}.k-upload-error-list,.k-upload-list{line-height:1.5em;font-size:var(--text-sm)}.k-upload-list-filename{color:var(--color-gray-600)}.k-upload-error-list li{padding:.75rem;background:var(--color-white);border-radius:var(--rounded-xs)}.k-upload-error-list li:not(:last-child){margin-bottom:2px}.k-upload-error-filename{color:var(--color-negative);font-weight:var(--font-bold)}.k-upload-error-message{color:var(--color-gray-600)}.k-writer-toolbar{position:absolute;display:flex;background:var(--color-black);height:30px;transform:translate(-50%) translateY(-.75rem);z-index:calc(var(--z-dropdown) + 1);box-shadow:var(--shadow);color:var(--color-white);border-radius:var(--rounded)}.k-writer-toolbar-button.k-button{display:flex;align-items:center;justify-content:center;height:30px;width:30px;font-size:var(--text-sm)!important;color:currentColor;line-height:1}.k-writer-toolbar-button.k-button:hover{background:rgba(255,255,255,.15)}.k-writer-toolbar-button.k-writer-toolbar-button-active{color:var(--color-blue-300)}.k-writer-toolbar-button.k-writer-toolbar-nodes{width:auto;padding:0 .75rem}[dir=ltr] .k-writer-toolbar .k-dropdown+.k-writer-toolbar-button{border-left:1px solid var(--color-gray-700)}[dir=rtl] .k-writer-toolbar .k-dropdown+.k-writer-toolbar-button{border-right:1px solid var(--color-gray-700)}[dir=ltr] .k-writer-toolbar-button.k-writer-toolbar-nodes:after{margin-left:.5rem}[dir=rtl] .k-writer-toolbar-button.k-writer-toolbar-nodes:after{margin-right:.5rem}.k-writer-toolbar-button.k-writer-toolbar-nodes:after{content:"";border-top:4px solid var(--color-white);border-left:4px solid transparent;border-right:4px solid transparent}.k-writer-toolbar .k-dropdown-content{color:var(--color-black);background:var(--color-white);margin-top:.5rem}.k-writer-toolbar .k-dropdown-content .k-dropdown-item[aria-current]{color:var(--color-focus);font-weight:500}.k-writer{position:relative;width:100%;grid-template-areas:"content";display:grid}.k-writer .ProseMirror{overflow-wrap:break-word;word-wrap:break-word;word-break:break-word;white-space:pre-wrap;font-variant-ligatures:none;line-height:inherit;grid-area:content}.k-writer .ProseMirror:focus{outline:0}.k-writer .ProseMirror *{caret-color:currentColor}.k-writer .ProseMirror a{color:var(--color-focus);text-decoration:underline}.k-writer .ProseMirror>:last-child{margin-bottom:0}.k-writer .ProseMirror h1,.k-writer .ProseMirror h2,.k-writer .ProseMirror h3,.k-writer .ProseMirror ol,.k-writer .ProseMirror p,.k-writer .ProseMirror ul{margin-bottom:.75rem}.k-writer .ProseMirror h1{font-size:var(--text-3xl);line-height:1.25em}.k-writer .ProseMirror h2{font-size:var(--text-2xl);line-height:1.25em}.k-writer .ProseMirror h3{font-size:var(--text-xl);line-height:1.25em}.k-writer .ProseMirror h1 strong,.k-writer .ProseMirror h2 strong,.k-writer .ProseMirror h3 strong{font-weight:700}.k-writer .ProseMirror strong{font-weight:600}.k-writer .ProseMirror code{position:relative;font-size:.925em;display:inline-block;line-height:1.325;padding:.05em .325em;background:var(--color-gray-300);border-radius:var(--rounded)}[dir=ltr] .k-writer .ProseMirror ol,[dir=ltr] .k-writer .ProseMirror ul{padding-left:1.75rem}[dir=rtl] .k-writer .ProseMirror ol,[dir=rtl] .k-writer .ProseMirror ul{padding-right:1.75rem}.k-writer .ProseMirror ul>li{list-style:disc}.k-writer .ProseMirror ul ul>li{list-style:circle}.k-writer .ProseMirror ul ul ul>li{list-style:square}.k-writer .ProseMirror ol>li{list-style:decimal}.k-writer .ProseMirror li>ol,.k-writer .ProseMirror li>p,.k-writer .ProseMirror li>ul{margin:0}.k-writer-code pre{-moz-tab-size:2;-o-tab-size:2;tab-size:2;font-size:var(--text-sm);line-height:2em;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;white-space:pre}.k-writer .ProseMirror code,.k-writer-code code{font-family:var(--font-mono)}.k-writer[data-placeholder][data-empty=true]:before{grid-area:content;content:attr(data-placeholder);line-height:inherit;color:var(--color-gray-500);pointer-events:none;white-space:pre-wrap;word-wrap:break-word}.k-login-alert{padding:.5rem .75rem;display:flex;justify-content:space-between;align-items:center;min-height:38px;margin-bottom:2rem;background:var(--color-negative);color:var(--color-white);font-size:var(--text-sm);border-radius:var(--rounded-xs);box-shadow:var(--shadow-lg);cursor:pointer}.k-structure-backdrop{position:absolute;top:0;right:0;bottom:0;left:0;z-index:2;height:100vh}.k-structure-form section{position:relative;z-index:3;border-radius:var(--rounded-xs);margin-bottom:1px;box-shadow:#1111110d 0 0 0 3px;border:1px solid var(--color-border);background:var(--color-background)}.k-structure-form-fields{padding:1.5rem 1.5rem 2rem}.k-structure-form-buttons{border-top:1px solid var(--color-border);display:flex;justify-content:space-between}.k-structure-form-buttons .k-pagination{display:none}@media screen and (min-width:65em){.k-structure-form-buttons .k-pagination{display:flex}}.k-structure-form-buttons .k-pagination>.k-button,.k-structure-form-buttons .k-pagination>span{padding:.875rem 1rem!important}.k-structure-form-cancel-button,.k-structure-form-submit-button{padding:.875rem 1.5rem;line-height:1rem;display:flex}[dir=ltr] .k-toolbar,[dir=rtl] .k-toolbar{border-top-right-radius:var(--rounded);border-top-left-radius:var(--rounded)}.k-toolbar{background:var(--color-white);border-bottom:1px solid var(--color-background);height:38px}.k-toolbar-wrapper{position:absolute;top:0;left:0;right:0;max-width:100%}.k-toolbar-buttons{display:flex}.k-toolbar-divider{width:1px;background:var(--color-background)}.k-toolbar-button{width:36px;height:36px}.k-toolbar-button:hover{background:rgba(239,239,239,.5)}.k-checkbox-input{position:relative;cursor:pointer}[dir=ltr] .k-checkbox-input-label{padding-left:1.75rem}[dir=rtl] .k-checkbox-input-label{padding-right:1.75rem}.k-checkbox-input-label{display:block}[dir=ltr] .k-checkbox-input-icon{left:0}[dir=rtl] .k-checkbox-input-icon{right:0}.k-checkbox-input-icon{position:absolute;width:1rem;height:1rem;border-radius:var(--rounded);border:2px solid var(--color-gray-500)}.k-checkbox-input-icon svg{position:absolute;width:12px;height:12px;display:none}.k-checkbox-input-icon path{stroke:var(--color-white)}.k-checkbox-input-native:checked+.k-checkbox-input-icon{border-color:var(--color-gray-900);background:var(--color-gray-900)}[data-disabled=true] .k-checkbox-input-native:checked+.k-checkbox-input-icon{border-color:var(--color-gray-600);background:var(--color-gray-600)}.k-checkbox-input-native:checked+.k-checkbox-input-icon svg{display:block}.k-checkbox-input-native:focus+.k-checkbox-input-icon{border-color:var(--color-blue-600)}.k-checkbox-input-native:focus:checked+.k-checkbox-input-icon{background:var(--color-focus)}.k-text-input{width:100%;border:0;background:0 0;font:inherit;color:inherit;font-variant-numeric:tabular-nums}.k-text-input::-moz-placeholder{color:var(--color-gray-500)}.k-text-input::placeholder{color:var(--color-gray-500)}.k-text-input:focus{outline:0}.k-text-input:invalid{box-shadow:none;outline:0}.k-list-input .ProseMirror{line-height:1.5em}.k-list-input .ProseMirror ol>li::marker{font-size:var(--text-sm);color:var(--color-gray-500)}.k-multiselect-input{display:flex;flex-wrap:wrap;position:relative;font-size:var(--text-sm);min-height:2.25rem;line-height:1}.k-multiselect-input .k-sortable-ghost{background:var(--color-focus)}.k-multiselect-input .k-tag{border-radius:var(--rounded-sm)}.k-multiselect-input .k-dropdown-content,.k-multiselect-input[data-layout=list] .k-tag{width:100%}.k-multiselect-search{margin-top:0!important;color:var(--color-white);background:var(--color-gray-900);border-bottom:1px dashed rgba(255,255,255,.2)}.k-multiselect-search>.k-button-text{flex:1;opacity:1!important}.k-multiselect-search input{width:100%;color:var(--color-white);background:0 0;border:0;outline:0;padding:.25rem 0;font:inherit}.k-multiselect-options{position:relative;max-height:275px;padding:.5rem 0}.k-multiselect-option{position:relative}.k-multiselect-option.selected{color:var(--color-positive-light)}.k-multiselect-option.disabled:not(.selected) .k-icon{opacity:0}.k-multiselect-option b{color:var(--color-focus-light);font-weight:700}[dir=ltr] .k-multiselect-value{margin-left:.25rem}[dir=rtl] .k-multiselect-value{margin-right:.25rem}.k-multiselect-value{color:var(--color-gray-500)}.k-multiselect-value:before{content:" ("}.k-multiselect-value:after{content:")"}[dir=ltr] .k-multiselect-input[data-layout=list] .k-tag{margin-right:0!important}[dir=rtl] .k-multiselect-input[data-layout=list] .k-tag{margin-left:0!important}.k-multiselect-more{width:100%;padding:.75rem;color:#fffc;text-align:center;border-top:1px dashed rgba(255,255,255,.2)}.k-multiselect-more:hover{color:var(--color-white)}.k-number-input{width:100%;border:0;background:0 0;font:inherit;color:inherit}.k-number-input::-moz-placeholder{color:var(--color-gray-500)}.k-number-input::placeholder{color:var(--color-gray-500)}.k-number-input:focus{outline:0}.k-number-input:invalid{box-shadow:none;outline:0}[dir=ltr] .k-radio-input li{padding-left:1.75rem}[dir=rtl] .k-radio-input li{padding-right:1.75rem}.k-radio-input li{position:relative;line-height:1.5rem}.k-radio-input input{position:absolute;width:0;height:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;opacity:0}.k-radio-input label{cursor:pointer;align-items:center}[dir=ltr] .k-radio-input label:before{left:0}[dir=rtl] .k-radio-input label:before{right:0}.k-radio-input label:before{position:absolute;top:.175em;content:"";width:1rem;height:1rem;border-radius:50%;border:2px solid var(--color-gray-500);box-shadow:var(--color-white) 0 0 0 2px inset}.k-radio-input input:checked+label:before{border-color:var(--color-gray-900);background:var(--color-gray-900)}[data-disabled=true] .k-radio-input input:checked+label:before{border-color:var(--color-gray-600);background:var(--color-gray-600)}.k-radio-input input:focus+label:before{border-color:var(--color-blue-600)}.k-radio-input input:focus:checked+label:before{background:var(--color-focus)}.k-radio-input-text{display:block}.k-range-input{--range-thumb-size:16px;--range-thumb-border:4px solid var(--color-gray-900);--range-thumb-border-disabled:4px solid var(--color-gray-600);--range-thumb-background:var(--color-background);--range-thumb-focus-border:4px solid var(--color-focus);--range-thumb-focus-background:var(--color-background);--range-track-height:4px;--range-track-background:var(--color-border);--range-track-color:var(--color-gray-900);--range-track-color-disabled:var(--color-gray-600);--range-track-focus-color:var(--color-focus);display:flex;align-items:center}.k-range-input-native{--min:0;--max:100;--value:0;--range:calc(var(--max) - var(--min));--ratio:calc((var(--value) - var(--min)) / var(--range));--position:calc( .5 * var(--range-thumb-size) + var(--ratio) * calc(100% - var(--range-thumb-size)) );-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:var(--range-thumb-size);background:0 0;font-size:var(--text-sm);line-height:1}.k-range-input-native::-webkit-slider-thumb{-webkit-appearance:none;appearance:none}.k-range-input-native::-webkit-slider-runnable-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background)}.k-range-input-native::-moz-range-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background)}.k-range-input-native::-ms-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background)}.k-range-input-native::-webkit-slider-runnable-track{background:linear-gradient(var(--range-track-color),var(--range-track-color))0/var(--position) 100%no-repeat var(--range-track-background)}.k-range-input-native::-moz-range-progress{height:var(--range-track-height);background:var(--range-track-color)}.k-range-input-native::-ms-fill-lower{height:var(--range-track-height);background:var(--range-track-color)}.k-range-input-native::-webkit-slider-thumb{margin-top:calc(.5*(var(--range-track-height) - var(--range-thumb-size)));box-sizing:border-box;width:var(--range-thumb-size);height:var(--range-thumb-size);background:var(--range-thumb-background);border:var(--range-thumb-border);border-radius:50%;cursor:pointer}.k-range-input-native::-moz-range-thumb{box-sizing:border-box;width:var(--range-thumb-size);height:var(--range-thumb-size);background:var(--range-thumb-background);border:var(--range-thumb-border);border-radius:50%;cursor:pointer}.k-range-input-native::-ms-thumb{box-sizing:border-box;width:var(--range-thumb-size);height:var(--range-thumb-size);background:var(--range-thumb-background);border:var(--range-thumb-border);border-radius:50%;cursor:pointer;margin-top:0}.k-range-input-native::-ms-tooltip{display:none}.k-range-input-native:focus{outline:0}.k-range-input-native:focus::-webkit-slider-runnable-track{border:0;border-radius:var(--range-track-height);width:100%;height:var(--range-track-height);background:var(--range-track-background);background:linear-gradient(var(--range-track-focus-color),var(--range-track-focus-color))0/var(--position) 100%no-repeat var(--range-track-background)}.k-range-input-native:focus::-moz-range-progress{height:var(--range-track-height);background:var(--range-track-focus-color)}.k-range-input-native:focus::-ms-fill-lower{height:var(--range-track-height);background:var(--range-track-focus-color)}.k-range-input-native:focus::-webkit-slider-thumb{background:var(--range-thumb-focus-background);border:var(--range-thumb-focus-border)}.k-range-input-native:focus::-moz-range-thumb{background:var(--range-thumb-focus-background);border:var(--range-thumb-focus-border)}.k-range-input-native:focus::-ms-thumb{background:var(--range-thumb-focus-background);border:var(--range-thumb-focus-border)}[dir=ltr] .k-range-input-tooltip{margin-left:1rem}[dir=rtl] .k-range-input-tooltip{margin-right:1rem}.k-range-input-tooltip{position:relative;max-width:20%;display:flex;align-items:center;color:var(--color-white);font-size:var(--text-xs);line-height:1;text-align:center;border-radius:var(--rounded-xs);background:var(--color-gray-900);padding:0 .25rem;white-space:nowrap}[dir=ltr] .k-range-input-tooltip:after{left:-5px}[dir=rtl] .k-range-input-tooltip:after{right:-5px}[dir=ltr] .k-range-input-tooltip:after{border-right:5px solid var(--color-gray-900)}[dir=rtl] .k-range-input-tooltip:after{border-left:5px solid var(--color-gray-900)}.k-range-input-tooltip:after{position:absolute;top:50%;width:0;height:0;transform:translateY(-50%);border-top:5px solid transparent;border-bottom:5px solid transparent;content:""}.k-range-input-tooltip>*{padding:4px}[data-disabled=true] .k-range-input-native::-webkit-slider-runnable-track{background:linear-gradient(var(--range-track-color-disabled),var(--range-track-color-disabled))0/var(--position) 100%no-repeat var(--range-track-background)}[data-disabled=true] .k-range-input-native::-moz-range-progress{height:var(--range-track-height);background:var(--range-track-color-disabled)}[data-disabled=true] .k-range-input-native::-ms-fill-lower{height:var(--range-track-height);background:var(--range-track-color-disabled)}[data-disabled=true] .k-range-input-native::-webkit-slider-thumb{border:var(--range-thumb-border-disabled)}[data-disabled=true] .k-range-input-native::-moz-range-thumb{border:var(--range-thumb-border-disabled)}[data-disabled=true] .k-range-input-native::-ms-thumb{border:var(--range-thumb-border-disabled)}[data-disabled=true] .k-range-input-tooltip{background:var(--color-gray-600)}[dir=ltr] [data-disabled=true] .k-range-input-tooltip:after{border-right:5px solid var(--color-gray-600)}[dir=rtl] [data-disabled=true] .k-range-input-tooltip:after{border-left:5px solid var(--color-gray-600)}.k-select-input{position:relative;display:block;cursor:pointer;overflow:hidden}.k-select-input-native{position:absolute;top:0;right:0;bottom:0;left:0;opacity:0;width:100%;font:inherit;z-index:1;cursor:pointer;-webkit-appearance:none;-moz-appearance:none;appearance:none;font-weight:var(--font-normal)}.k-select-input-native[disabled]{cursor:default}.k-tags-input{display:flex;flex-wrap:wrap}.k-tags-input .k-tag{border-radius:var(--rounded-sm)}.k-tags-input .k-sortable-ghost{background:var(--color-focus)}.k-tags-input-element{flex-grow:1;flex-basis:0;min-width:0}.k-tags-input:focus-within .k-tags-input-element{flex-basis:4rem}.k-tags-input-element input{font:inherit;border:0;width:100%;background:0 0}.k-tags-input-element input:focus{outline:0}[dir=ltr] .k-tags-input[data-layout=list] .k-tag{margin-right:0!important}[dir=rtl] .k-tags-input[data-layout=list] .k-tag{margin-left:0!important}.k-tags-input[data-layout=list] .k-tag{width:100%}.k-textarea-input[data-size=small]{--size:7.5rem}.k-textarea-input[data-size=medium]{--size:15rem}.k-textarea-input[data-size=large]{--size:30rem}.k-textarea-input[data-size=huge]{--size:45rem}.k-textarea-input-wrapper{position:relative}.k-textarea-input-native{resize:none;border:0;width:100%;background:0 0;font:inherit;line-height:1.5em;color:inherit;min-height:var(--size)}.k-textarea-input-native::-moz-placeholder{color:var(--color-gray-500)}.k-textarea-input-native::placeholder{color:var(--color-gray-500)}.k-textarea-input-native:focus{outline:0}.k-textarea-input-native:invalid{box-shadow:none;outline:0}.k-textarea-input-native[data-font=monospace]{font-family:var(--font-mono)}.k-toolbar{margin-bottom:.25rem;color:#aaa}.k-textarea-input:focus-within .k-toolbar{position:sticky;top:0;left:0;right:0;z-index:1;box-shadow:#0000000d 0 2px 5px;border-bottom:1px solid rgba(0,0,0,.1);color:#000}.k-toggle-input{--toggle-background:var(--color-white);--toggle-color:var(--color-gray-500);--toggle-active-color:var(--color-gray-900);--toggle-focus-color:var(--color-focus);--toggle-height:16px;display:flex;align-items:center}.k-toggle-input-native{position:relative;height:var(--toggle-height);width:calc(var(--toggle-height)*2);border-radius:var(--toggle-height);border:2px solid var(--toggle-color);box-shadow:inset 0 0 0 2px var(--toggle-background),inset calc(var(--toggle-height)*-1) 0 0 2px var(--toggle-background);background-color:var(--toggle-color);outline:0;transition:all ease-in-out .1s;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;flex-shrink:0}.k-toggle-input-native:checked{border-color:var(--toggle-active-color);box-shadow:inset 0 0 0 2px var(--toggle-background),inset var(--toggle-height) 0 0 2px var(--toggle-background);background-color:var(--toggle-active-color)}.k-toggle-input-native[disabled]{border-color:var(--color-border);box-shadow:inset 0 0 0 2px var(--color-background),inset calc(var(--toggle-height)*-1) 0 0 2px var(--color-background);background-color:var(--color-border)}.k-toggle-input-native[disabled]:checked{box-shadow:inset 0 0 0 2px var(--color-background),inset var(--toggle-height) 0 0 2px var(--color-background)}.k-toggle-input-native:focus:checked{border:2px solid var(--color-focus);background-color:var(--toggle-focus-color)}.k-toggle-input-native::-ms-check{opacity:0}.k-toggle-input-label{cursor:pointer;flex-grow:1}.k-input[data-type=toggles]{display:inline-flex}.k-input[data-type=toggles].grow{display:flex}.k-toggles-input{display:grid;grid-template-columns:repeat(var(--options),minmax(0,1fr));gap:1px;border-radius:var(--rounded);line-height:1;background:var(--color-border);overflow:hidden}.k-toggles-input li{height:var(--field-input-height);background:var(--color-white)}.k-toggles-input label{align-items:center;background:var(--color-white);cursor:pointer;display:flex;font-size:var(--text-sm);justify-content:center;line-height:1.25;padding:0 var(--spacing-3);height:100%}[dir=ltr] .k-toggles-input .k-icon+.k-toggles-text{margin-left:var(--spacing-2)}[dir=rtl] .k-toggles-input .k-icon+.k-toggles-text{margin-right:var(--spacing-2)}.k-toggles-input input:focus:not(:checked)+label{background:var(--color-gray-200)}.k-toggles-input input:checked+label{background:var(--color-black);color:var(--color-white)}.k-blocks-field{position:relative}.k-date-field-body{display:flex;flex-wrap:wrap;line-height:1;border:var(--field-input-border);background:var(--color-gray-300);gap:1px;border-radius:var(--rounded);--multiplier:calc(25rem - 100%)}.k-date-field-body:focus-within{border:var(--field-input-focus-border);box-shadow:var(--color-focus-outline) 0 0 0 2px}.k-date-field[data-disabled] .k-date-field-body{background:0 0}.k-date-field-body>.k-input[data-theme=field]{border:0;box-shadow:none;border-radius:var(--rounded)}.k-date-field-body>.k-input[data-invalid=true],.k-date-field-body>.k-input[data-invalid=true]:focus-within{border:0!important;box-shadow:none!important}.k-date-field-body>*{flex-grow:1;flex-basis:calc(var(--multiplier)*999);max-width:100%}.k-date-field-body .k-input[data-type=date]{min-width:60%}.k-date-field-body .k-input[data-type=time]{min-width:30%}.k-files-field[data-disabled=true] *{pointer-events:all!important}body{counter-reset:headline-counter}.k-headline-field{position:relative;padding-top:1.5rem}.k-fieldset>.k-grid .k-column:first-child .k-headline-field{padding-top:0}[dir=ltr] .k-headline-field .k-headline[data-numbered]:before{padding-right:.25rem}[dir=rtl] .k-headline-field .k-headline[data-numbered]:before{padding-left:.25rem}.k-headline-field .k-headline[data-numbered]:before{counter-increment:headline-counter;content:counter(headline-counter,decimal-leading-zero);color:var(--color-focus);font-weight:400}.k-info-field .k-headline{padding-bottom:.75rem;line-height:1.25rem}.k-layout-column{position:relative;height:100%;display:flex;flex-direction:column;background:var(--color-white);min-height:6rem}.k-layout-column:focus{outline:0}.k-layout-column .k-blocks{background:0 0;box-shadow:none;padding:0;height:100%;background:var(--color-white);min-height:4rem}.k-layout-column .k-blocks[data-empty=true]{min-height:6rem}.k-layout-column .k-blocks-list{display:flex;flex-direction:column;height:100%}.k-layout-column .k-blocks .k-block-container:last-of-type{flex-grow:1}.k-layout-column .k-blocks-empty{position:absolute;top:0;right:0;bottom:0;left:0;justify-content:center;opacity:0;transition:opacity .3s;border:0}.k-layout-column .k-blocks-empty:hover{opacity:1}[dir=ltr] .k-layout-column .k-blocks-empty.k-empty .k-icon{border-right:0}[dir=rtl] .k-layout-column .k-blocks-empty.k-empty .k-icon{border-left:0}.k-layout-column .k-blocks-empty.k-empty .k-icon{width:1rem}[dir=ltr] .k-layout{padding-right:var(--layout-toolbar-width)}[dir=rtl] .k-layout{padding-left:var(--layout-toolbar-width)}.k-layout{--layout-border-color:var(--color-gray-300);--layout-toolbar-width:2rem;position:relative;background:#fff;box-shadow:var(--shadow)}[dir=ltr] [data-disabled=true] .k-layout{padding-right:0}[dir=rtl] [data-disabled=true] .k-layout{padding-left:0}.k-layout:not(:last-of-type){margin-bottom:1px}.k-layout:focus{outline:0}[dir=ltr] .k-layout-toolbar{right:0}[dir=rtl] .k-layout-toolbar{left:0}[dir=ltr] .k-layout-toolbar{border-left:1px solid var(--color-light)}[dir=rtl] .k-layout-toolbar{border-right:1px solid var(--color-light)}.k-layout-toolbar{position:absolute;top:0;bottom:0;width:var(--layout-toolbar-width);display:flex;flex-direction:column;font-size:var(--text-sm);background:var(--color-gray-100);color:var(--color-gray-500)}.k-layout-toolbar:hover{color:var(--color-black)}.k-layout-toolbar-button{width:var(--layout-toolbar-width);height:var(--layout-toolbar-width)}.k-layout-toolbar .k-sort-handle{margin-top:auto;color:currentColor}.k-layout-columns.k-grid{grid-gap:1px;background:var(--layout-border-color);background:var(--color-gray-300)}.k-layout:not(:first-child) .k-layout-columns.k-grid{border-top:0}.k-layouts .k-sortable-ghost{position:relative;box-shadow:#11111140 0 5px 10px;outline:2px solid var(--color-focus);cursor:grabbing;cursor:-webkit-grabbing;z-index:1}.k-layout-selector.k-dialog{background:#313740;color:var(--color-white)}.k-layout-selector .k-headline{line-height:1;margin-top:-.25rem;margin-bottom:1.5rem}.k-layout-selector ul{display:grid;grid-template-columns:repeat(3,1fr);grid-gap:1.5rem}.k-layout-selector-option .k-grid{height:5rem;grid-gap:2px;box-shadow:var(--shadow);cursor:pointer}.k-layout-selector-option:hover{outline:2px solid var(--color-green-300);outline-offset:2px}.k-layout-selector-option:last-child{margin-bottom:0}.k-layout-selector-option .k-column{display:flex;background:rgba(255,255,255,.2);justify-content:center;font-size:var(--text-xs);align-items:center}.k-layout-add-button{display:flex;align-items:center;width:100%;color:var(--color-gray-500);justify-content:center;padding:.75rem 0}.k-layout-add-button:hover{color:var(--color-black)}.k-line-field{position:relative;border:0;height:3rem;width:auto}.k-line-field:after{position:absolute;content:"";top:50%;margin-top:-1px;left:0;right:0;height:1px;background:var(--color-border)}.k-list-field .k-list-input{padding:.375rem .5rem .375rem .75rem}.k-pages-field[data-disabled=true] *{pointer-events:all!important}.k-structure-field:not([data-disabled=true]) td.k-table-column{cursor:pointer}.k-field-counter{display:none}.k-text-field:focus-within .k-field-counter{display:block}.k-users-field[data-disabled=true] *{pointer-events:all!important}.k-writer-field-input{line-height:1.5em;padding:.375rem .5rem}.k-aspect-ratio{position:relative;display:block;overflow:hidden;padding-bottom:100%}.k-aspect-ratio>*{position:absolute!important;top:0;right:0;bottom:0;left:0;height:100%;width:100%;-o-object-fit:contain;object-fit:contain}.k-aspect-ratio[data-cover=true]>*{-o-object-fit:cover;object-fit:cover}.k-bar{display:flex;align-items:center;justify-content:space-between;line-height:1}.k-bar-slot{flex-grow:1}.k-bar-slot[data-position=center]{text-align:center}[dir=ltr] .k-bar-slot[data-position=right]{text-align:right}[dir=rtl] .k-bar-slot[data-position=right]{text-align:left}.k-box{word-wrap:break-word;font-size:var(--text-sm)}.k-box:not([data-theme=none]){background:var(--color-white);border-radius:var(--rounded);line-height:1.25rem;padding:.5rem .75rem}.k-box[data-theme=code]{background:var(--color-gray-900);border:1px solid var(--color-black);color:var(--color-light);font-family:Input,Menlo,monospace;font-size:var(--text-sm);line-height:1.5}.k-box[data-theme=button]{padding:0}[dir=ltr] .k-box[data-theme=button] .k-button{text-align:left}[dir=rtl] .k-box[data-theme=button] .k-button{text-align:right}.k-box[data-theme=button] .k-button{padding:0 .75rem;height:2.25rem;width:100%;display:flex;align-items:center;line-height:2rem}[dir=ltr] .k-box[data-theme=info],[dir=ltr] .k-box[data-theme=negative],[dir=ltr] .k-box[data-theme=notice],[dir=ltr] .k-box[data-theme=positive]{border-left-color:var(--theme-light)}[dir=rtl] .k-box[data-theme=info],[dir=rtl] .k-box[data-theme=negative],[dir=rtl] .k-box[data-theme=notice],[dir=rtl] .k-box[data-theme=positive]{border-right-color:var(--theme-light)}.k-box[data-theme=info],.k-box[data-theme=negative],.k-box[data-theme=notice],.k-box[data-theme=positive]{border:0;background:var(--theme-bg)}[dir=ltr] .k-box[data-theme=empty]{border-left:0}[dir=rtl] .k-box[data-theme=empty]{border-right:0}.k-box[data-theme=empty]{text-align:center;padding:3rem 1.5rem;display:flex;justify-content:center;align-items:center;flex-direction:column;background:var(--color-background);border:1px dashed var(--color-border)}.k-box[data-theme=empty] .k-icon{margin-bottom:.5rem;color:var(--color-gray-500)}.k-box[data-theme=empty],.k-box[data-theme=empty] p{color:var(--color-gray-600)}.k-bubble{display:flex;padding:0 .5rem;white-space:nowrap;align-items:center;line-height:1.5;font-size:var(--text-xs);height:1.525rem;background:var(--color-light);color:var(--color-black);border-radius:var(--rounded);overflow:hidden}[dir=ltr] .k-bubble .k-item-figure{margin-left:-.5rem}[dir=rtl] .k-bubble .k-item-figure{margin-right:-.5rem}[dir=ltr] .k-bubble .k-item-figure{margin-right:var(--spacing-2)}[dir=rtl] .k-bubble .k-item-figure{margin-left:var(--spacing-2)}.k-bubble .k-item-figure{width:1.525rem;height:1.525rem}.k-bubbles{display:flex;gap:.25rem}.k-collection-help{padding:.5rem .75rem}.k-collection-footer{display:flex;justify-content:space-between;margin-left:-.75rem;margin-right:-.75rem}.k-collection-pagination{line-height:1.25rem;flex-shrink:0;min-height:2.75rem}.k-collection-pagination .k-pagination .k-button{padding:.5rem .75rem;line-height:1.125rem}.k-column{min-width:0;grid-column-start:span 12}.k-column[data-sticky=true]>div{position:sticky;top:4vh;z-index:2}@media screen and (min-width:65em){.k-column[data-width="1/1"],.k-column[data-width="12/12"],.k-column[data-width="2/2"],.k-column[data-width="3/3"],.k-column[data-width="4/4"],.k-column[data-width="6/6"]{grid-column-start:span 12}.k-column[data-width="11/12"]{grid-column-start:span 11}.k-column[data-width="10/12"],.k-column[data-width="5/6"]{grid-column-start:span 10}.k-column[data-width="3/4"],.k-column[data-width="9/12"]{grid-column-start:span 9}.k-column[data-width="2/3"],.k-column[data-width="4/6"],.k-column[data-width="8/12"]{grid-column-start:span 8}.k-column[data-width="7/12"]{grid-column-start:span 7}.k-column[data-width="1/2"],.k-column[data-width="2/4"],.k-column[data-width="3/6"],.k-column[data-width="6/12"]{grid-column-start:span 6}.k-column[data-width="5/12"]{grid-column-start:span 5}.k-column[data-width="1/3"],.k-column[data-width="2/6"],.k-column[data-width="4/12"]{grid-column-start:span 4}.k-column[data-width="1/4"],.k-column[data-width="3/12"]{grid-column-start:span 3}.k-column[data-width="1/6"],.k-column[data-width="2/12"]{grid-column-start:span 2}.k-column[data-width="1/12"]{grid-column-start:span 1}}.k-column[data-disabled=true]{cursor:not-allowed;opacity:.4}.k-column[data-disabled=true] *{pointer-events:none}.k-column[data-disabled=true] .k-text[data-theme=help] *{pointer-events:initial}.k-dropzone{position:relative}.k-dropzone:after{content:"";position:absolute;top:0;right:0;bottom:0;left:0;display:none;pointer-events:none;z-index:1}.k-dropzone[data-over=true]:after{display:block;outline:1px solid var(--color-focus);box-shadow:var(--color-focus-outline) 0 0 0 3px}.k-empty{display:flex;align-items:stretch;border-radius:var(--rounded);color:var(--color-gray-600);border:1px dashed var(--color-border)}button.k-empty{width:100%}button.k-empty:focus{outline:0}.k-empty p{font-size:var(--text-sm);color:var(--color-gray-600)}.k-empty>.k-icon{color:var(--color-gray-500)}.k-empty[data-layout=cardlets],.k-empty[data-layout=cards]{text-align:center;padding:1.5rem;justify-content:center;flex-direction:column}.k-empty[data-layout=cardlets] .k-icon,.k-empty[data-layout=cards] .k-icon{margin-bottom:1rem}.k-empty[data-layout=cardlets] .k-icon svg,.k-empty[data-layout=cards] .k-icon svg{width:2rem;height:2rem}.k-empty[data-layout=list],.k-empty[data-layout=table]{min-height:38px}[dir=ltr] .k-empty[data-layout=list]>.k-icon,[dir=ltr] .k-empty[data-layout=table]>.k-icon{border-right:1px solid rgba(0,0,0,.05)}[dir=rtl] .k-empty[data-layout=list]>.k-icon,[dir=rtl] .k-empty[data-layout=table]>.k-icon{border-left:1px solid rgba(0,0,0,.05)}.k-empty[data-layout=list]>.k-icon,.k-empty[data-layout=table]>.k-icon{width:36px;min-height:36px}.k-empty[data-layout=list]>p,.k-empty[data-layout=table]>p{line-height:1.25rem;padding:.5rem .75rem}.k-file-preview{background:var(--color-gray-800)}.k-file-preview-layout{display:grid;grid-template-columns:50%auto}.k-file-preview-layout>*{min-width:0}.k-file-preview-image{position:relative;display:flex;align-items:center;justify-content:center;background:var(--bg-pattern)}.k-file-preview-image-link{display:block;width:100%;padding:min(4vw,3rem);outline:0}.k-file-preview-image-link[data-tabbed=true]{box-shadow:none;outline:2px solid var(--color-focus);outline-offset:-2px}.k-file-preview-details{padding:1.5rem;flex-grow:1}.k-file-preview-details ul{line-height:1.5em;max-width:50rem;display:grid;grid-gap:1.5rem 3rem;grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}.k-file-preview-details h3{font-size:var(--text-sm);font-weight:500;color:var(--color-gray-500)}.k-file-preview-details a,.k-file-preview-details p{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:#ffffffbf;font-size:var(--text-sm)}@media screen and (min-width:30em){.k-file-preview-details ul{grid-template-columns:repeat(auto-fill,minmax(200px,1fr))}}@media screen and (max-width:65em){.k-file-preview-layout{padding:0!important}}@media screen and (min-width:65em){.k-file-preview-layout{grid-template-columns:33.333%auto;align-items:center}.k-file-preview-details{padding:3rem}}@media screen and (min-width:90em){.k-file-preview-layout{grid-template-columns:25%auto}}.k-grid{--columns:12;display:grid;grid-column-gap:0;grid-row-gap:0;grid-template-columns:1fr}@media screen and (min-width:30em){.k-grid[data-gutter=small]{grid-column-gap:1rem;grid-row-gap:1rem}.k-grid[data-gutter=huge],.k-grid[data-gutter=large],.k-grid[data-gutter=medium]{grid-column-gap:1.5rem;grid-row-gap:1.5rem}}@media screen and (min-width:65em){.k-grid{grid-template-columns:repeat(var(--columns),1fr)}.k-grid[data-gutter=large]{grid-column-gap:3rem}.k-grid[data-gutter=huge]{grid-column-gap:4.5rem}}@media screen and (min-width:90em){.k-grid[data-gutter=large]{grid-column-gap:4.5rem}.k-grid[data-gutter=huge]{grid-column-gap:6rem}}@media screen and (min-width:120em){.k-grid[data-gutter=large]{grid-column-gap:6rem}.k-grid[data-gutter=huge]{grid-column-gap:7.5rem}}.k-header{padding-top:4vh;margin-bottom:2rem;border-bottom:1px solid var(--color-border)}.k-header[data-tabs=true]{border-bottom:0}.k-header .k-headline{min-height:1.25em;margin-bottom:.5rem;word-wrap:break-word}.k-header .k-header-buttons{margin-top:-.5rem;height:3.25rem}.k-header .k-headline-editable{cursor:pointer}[dir=ltr] .k-header .k-headline-editable .k-icon{margin-left:.5rem}[dir=rtl] .k-header .k-headline-editable .k-icon{margin-right:.5rem}.k-header .k-headline-editable .k-icon{color:var(--color-gray-500);opacity:0;transition:opacity .3s;display:inline-block}.k-header .k-headline-editable:hover .k-icon{opacity:1}.k-panel-inside{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column}.k-panel-inside:focus{outline:0}.k-panel-header{z-index:var(--z-navigation);flex-shrink:0}.k-panel-view{flex-grow:1;padding-bottom:6rem}.k-item{position:relative;background:var(--color-white);border-radius:var(--rounded);box-shadow:var(--shadow);display:grid;grid-template-columns:auto;line-height:1}.k-item a:focus,.k-item:focus{outline:0}.k-item:focus-within{box-shadow:var(--shadow-outline)}.k-item-sort-handle.k-sort-handle{position:absolute;opacity:0;width:1.25rem;height:1.5rem;z-index:2;border-radius:1px}.k-item:hover .k-item-sort-handle{opacity:1}.k-item-figure{grid-area:figure}.k-item-content{grid-area:content;overflow:hidden}.k-item-info,.k-item-title{font-size:var(--text-sm);font-weight:400;text-overflow:ellipsis;white-space:nowrap;line-height:1.125rem;overflow:hidden}.k-item-info{grid-area:info;color:var(--color-gray-500)}.k-item-title-link.k-link[data-=true]{box-shadow:none}.k-item-title-link:after{position:absolute;content:"";top:0;right:0;bottom:0;left:0;z-index:1}.k-item-footer{grid-area:footer;display:flex;justify-content:space-between;align-items:center;min-width:0}[dir=ltr] .k-item-label{margin-right:.5rem}[dir=rtl] .k-item-label{margin-left:.5rem}.k-item-buttons{position:relative;display:flex;justify-content:flex-end;flex-shrink:0;flex-grow:1}.k-item-buttons>.k-button,.k-item-buttons>.k-dropdown{position:relative;width:38px;height:38px;display:flex!important;align-items:center;justify-content:center;line-height:1}.k-item-buttons>.k-button{z-index:1}.k-item-buttons>.k-options-dropdown>.k-options-dropdown-toggle{z-index:var(--z-toolbar)}.k-list-item{display:flex;align-items:center;height:38px}[dir=ltr] .k-list-item .k-item-sort-handle{left:-1.5rem}[dir=rtl] .k-list-item .k-item-sort-handle{right:-1.5rem}.k-list-item .k-item-sort-handle{width:1.5rem}[dir=ltr] .k-list-item .k-item-figure{border-top-left-radius:var(--rounded)}[dir=rtl] .k-list-item .k-item-figure{border-top-right-radius:var(--rounded)}[dir=ltr] .k-list-item .k-item-figure{border-bottom-left-radius:var(--rounded)}[dir=rtl] .k-list-item .k-item-figure{border-bottom-right-radius:var(--rounded)}.k-list-item .k-item-figure{width:38px}[dir=ltr] .k-list-item .k-item-content{margin-left:.75rem}[dir=rtl] .k-list-item .k-item-content{margin-right:.75rem}.k-list-item .k-item-content{display:flex;flex-grow:1;flex-shrink:2;justify-content:space-between;align-items:center}.k-list-item .k-item-info,.k-list-item .k-item-title{flex-grow:1;line-height:1.5rem}[dir=ltr] .k-list-item .k-item-title{margin-right:.5rem}[dir=rtl] .k-list-item .k-item-title{margin-left:.5rem}.k-list-item .k-item-title{flex-shrink:1}[dir=ltr] .k-list-item .k-item-info{text-align:right}[dir=rtl] .k-list-item .k-item-info{text-align:left}[dir=ltr] .k-list-item .k-item-info{margin-right:.5rem}[dir=rtl] .k-list-item .k-item-info{margin-left:.5rem}.k-list-item .k-item-info{flex-shrink:2;justify-self:end}.k-list-item .k-item-buttons,.k-list-item .k-item-footer{flex-shrink:0}.k-item:not(.k-list-item) .k-item-sort-handle{margin:var(--spacing-2);background:var(--color-background);box-shadow:var(--shadow-lg);border-radius:var(--rounded-sm)}[dir=ltr] .k-item:not(.k-list-item) .k-item-label{margin-left:-2px}[dir=rtl] .k-item:not(.k-list-item) .k-item-label{margin-right:-2px}.k-item:not(.k-list-item) .k-item-content{padding:.625rem .75rem}.k-cardlets-item{height:6rem;grid-template-rows:auto 38px;grid-template-areas:"content""footer"}.k-cardlets-item[data-has-figure=true]{grid-template-columns:6rem auto;grid-template-areas:"figure content""figure footer"}[dir=ltr] .k-cardlets-item .k-item-figure{border-top-left-radius:var(--rounded)}[dir=rtl] .k-cardlets-item .k-item-figure{border-top-right-radius:var(--rounded)}[dir=ltr] .k-cardlets-item .k-item-figure{border-bottom-left-radius:var(--rounded)}[dir=rtl] .k-cardlets-item .k-item-figure{border-bottom-right-radius:var(--rounded)}.k-cardlets-item .k-item-footer{padding-top:.5rem;padding-bottom:.5rem}.k-cards-item{grid-template-columns:auto;grid-template-rows:auto 1fr;grid-template-areas:"figure""content";--item-content-wrapper:0}[dir=ltr] .k-cards-item .k-item-figure,[dir=rtl] .k-cards-item .k-item-figure{border-top-right-radius:var(--rounded);border-top-left-radius:var(--rounded)}.k-cards-item .k-item-content{padding:.5rem .75rem!important;overflow:hidden}.k-cards-item .k-item-info,.k-cards-item .k-item-title{line-height:1.375rem;white-space:normal}.k-cards-item .k-item-info:after,.k-cards-item .k-item-title:after{display:inline-block;content:"\a0";width:var(--item-content-wrapper)}.k-cards-item[data-has-flag=true],.k-cards-item[data-has-options=true]{--item-content-wrapper:38px}.k-cards-item[data-has-flag=true][data-has-options=true]{--item-content-wrapper:76px}.k-cards-item[data-has-info=true] .k-item-title:after{display:none}[dir=ltr] .k-cards-item .k-item-footer{right:0}[dir=rtl] .k-cards-item .k-item-footer{left:0}.k-cards-item .k-item-footer{position:absolute;bottom:0;width:auto}.k-item-figure{overflow:hidden;flex-shrink:0}.k-cards-items{--min:13rem;--max:1fr;--gap:1.5rem;--column-gap:var(--gap);--row-gap:var(--gap);display:grid;grid-column-gap:var(--column-gap);grid-row-gap:var(--row-gap);grid-template-columns:repeat(auto-fill,minmax(var(--min),var(--max)))}@media screen and (min-width:30em){.k-cards-items[data-size=tiny]{--min:10rem}.k-cards-items[data-size=small]{--min:16rem}.k-cards-items[data-size=medium]{--min:24rem}.k-cards-items[data-size=huge],.k-cards-items[data-size=large],.k-column[data-width="1/4"] .k-cards-items,.k-column[data-width="1/5"] .k-cards-items,.k-column[data-width="1/6"] .k-cards-items{--min:1fr}}@media screen and (min-width:65em){.k-cards-items[data-size=large]{--min:32rem}}.k-cardlets-items{display:grid;grid-template-columns:repeat(auto-fill,minmax(16rem,1fr));grid-gap:.5rem}.k-list-items .k-list-item:not(:last-child){margin-bottom:2px}.k-overlay{position:fixed;top:0;right:0;bottom:0;left:0;width:100%;height:100%;z-index:var(--z-dialog);transform:translateZ(0)}.k-overlay[data-centered=true]{display:flex;align-items:center;justify-content:center}.k-overlay[data-dimmed=true]{background:var(--color-backdrop)}.k-overlay-loader{color:var(--color-white)}.k-panel[data-loading=true]{animation:LoadingCursor .5s}.k-panel[data-dragging=true],.k-panel[data-loading=true]:after{-webkit-user-select:none;-moz-user-select:none;user-select:none}.k-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));grid-gap:var(--spacing-2px)}.k-stat{display:flex;flex-direction:column;background:var(--color-white);box-shadow:var(--shadow);padding:var(--spacing-3) var(--spacing-6);line-height:var(--leading-normal);border-radius:var(--rounded)}.k-stat.k-link:hover,.k-stat[data-click=true]:hover{cursor:pointer;background:var(--color-gray-100)}.k-stat dd,.k-stat dt{display:block}.k-stat-value{font-size:var(--value);margin-bottom:var(--spacing-1);order:1}.k-stat-info,.k-stat-label{font-size:var(--text-xs)}.k-stat-label{order:2}.k-stat-info{order:3;color:var(--theme, var(--color-gray-500))}.k-stats[data-size=small]{--value:var(--text-base)}.k-stats[data-size=medium]{--value:var(--text-xl)}.k-stats[data-size=large]{--value:var(--text-2xl)}.k-stats[data-size=huge]{--value:var(--text-3xl)}.k-table{--table-row-height:38px;position:relative;table-layout:fixed;background:var(--color-white);font-size:var(--text-sm);border-spacing:0;box-shadow:var(--shadow);border-radius:var(--rounded);font-variant-numeric:tabular-nums}.k-table td,.k-table th{height:var(--table-row-height);overflow:hidden;text-overflow:ellipsis;line-height:1.25em}.k-table,.k-table td{width:100%}[dir=ltr] .k-table th:first-child{border-top-left-radius:var(--rounded)}[dir=rtl] .k-table th:first-child{border-top-right-radius:var(--rounded)}[dir=ltr] .k-table th:last-child{border-top-right-radius:var(--rounded)}[dir=rtl] .k-table th:last-child{border-top-left-radius:var(--rounded)}[dir=ltr] .k-table td:last-child,[dir=ltr] .k-table th:last-child{border-right:0}[dir=rtl] .k-table td:last-child,[dir=rtl] .k-table th:last-child{border-left:0}.k-table td:last-child,.k-table th:last-child{height:var(--table-row-height)}.k-table th,.k-table tr:not(:last-child) td{border-bottom:1px solid var(--color-background)}.k-table td:last-child{overflow:visible}[dir=ltr] .k-table td,[dir=ltr] .k-table th{border-right:1px solid var(--color-background)}[dir=rtl] .k-table td,[dir=rtl] .k-table th{border-left:1px solid var(--color-background)}.k-table tbody tr:hover td{background:rgba(239,239,239,.25)}.k-table-column[data-align]{text-align:var(--align)}.k-table-column[data-align=right]>.k-input{flex-direction:column;align-items:flex-end}.k-table th{position:sticky;top:0;left:0;right:0;width:100%;padding:0 .75rem;z-index:1;font-family:var(--font-mono);font-size:var(--text-xs);font-weight:400;color:var(--color-gray-600);background:var(--color-gray-100)}[dir=ltr] .k-table th{text-align:left}[dir=rtl] .k-table th{text-align:right}.k-table th:after{content:"";position:absolute;top:100%;left:0;right:0;height:.5rem;background:linear-gradient(to bottom,rgba(#000,.05),rgba(#000,0));z-index:2}.k-table .k-sort-handle,.k-table-index{display:grid;place-items:center;width:100%;height:var(--table-row-height)}.k-table .k-sort-handle,.k-table tr:hover .k-table-index-column[data-sortable=true] .k-table-index{display:none}.k-table tr:hover .k-sort-handle{display:grid!important}.k-table-row-ghost{background:var(--color-white);box-shadow:#11111140 0 5px 10px;outline:2px solid var(--color-focus);margin-bottom:2px;cursor:grabbing;cursor:-webkit-grabbing}.k-table-row-fallback{opacity:0!important}td.k-table-index-column,td.k-table-options-column,th.k-table-index-column,th.k-table-options-column{width:var(--table-row-height);text-align:center!important}.k-table-index{font-size:var(--text-xs);color:var(--color-gray-500);line-height:1.1em}.k-table-empty{color:var(--color-gray-600);font-size:var(--text-sm)}[data-disabled=true] .k-table{background:var(--color-background)}[dir=ltr] [data-disabled=true] .k-table td,[dir=ltr] [data-disabled=true] .k-table th{border-right:1px solid var(--color-border)}[dir=rtl] [data-disabled=true] .k-table td,[dir=rtl] [data-disabled=true] .k-table th{border-left:1px solid var(--color-border)}[data-disabled=true] .k-table td,[data-disabled=true] .k-table th{background:var(--color-background);border-bottom:1px solid var(--color-border)}[data-disabled=true] .k-table td:last-child{overflow:hidden;text-overflow:ellipsis}@media screen and (max-width:65em){.k-table td:not([data-mobile]),.k-table th:not([data-mobile]){display:none}}.k-tabs{position:relative;background:#e9e9e9;border:1px solid var(--color-border);border-radius:var(--rounded)}.k-tabs nav{display:flex;justify-content:center;margin-left:-1px;margin-right:-1px}[dir=ltr] .k-tab-button.k-button{border-left:1px solid transparent}[dir=rtl] .k-tab-button.k-button{border-right:1px solid transparent}[dir=ltr] .k-tab-button.k-button{border-right:1px solid var(--color-border)}[dir=rtl] .k-tab-button.k-button{border-left:1px solid var(--color-border)}.k-tab-button.k-button{position:relative;z-index:1;display:inline-flex;justify-content:center;align-items:center;padding:.625rem .75rem;font-size:var(--text-xs);text-transform:uppercase;text-align:center;font-weight:500;flex-grow:1;flex-shrink:1;flex-direction:column;max-width:15rem}@media screen and (min-width:30em){.k-tab-button.k-button{flex-direction:row}[dir=ltr] .k-tab-button.k-button .k-icon{margin-right:.5rem}[dir=rtl] .k-tab-button.k-button .k-icon{margin-left:.5rem}}[dir=ltr] .k-tab-button.k-button>.k-button-text{padding-left:0}[dir=rtl] .k-tab-button.k-button>.k-button-text{padding-right:0}.k-tab-button.k-button>.k-button-text{padding-top:.375rem;font-size:10px;overflow:hidden;max-width:10rem;text-overflow:ellipsis;opacity:1}@media screen and (min-width:30em){.k-tab-button.k-button>.k-button-text{font-size:var(--text-xs);padding-top:0}}[dir=ltr] .k-tab-button:last-child{border-right:1px solid transparent}[dir=rtl] .k-tab-button:last-child{border-left:1px solid transparent}[dir=ltr] .k-tab-button[aria-current]{border-right:1px solid var(--color-border)}[dir=rtl] .k-tab-button[aria-current]{border-left:1px solid var(--color-border)}.k-tab-button[aria-current]{position:relative;background:var(--color-background);pointer-events:none}[dir=ltr] .k-tab-button[aria-current]:first-child{border-left:1px solid var(--color-border)}[dir=rtl] .k-tab-button[aria-current]:first-child{border-right:1px solid var(--color-border)}.k-tab-button[aria-current]:after,.k-tab-button[aria-current]:before{position:absolute;content:""}.k-tab-button[aria-current]:before{left:-1px;right:-1px;top:-1px;height:2px;background:var(--color-black)}.k-tab-button[aria-current]:after{left:0;right:0;bottom:-1px;height:1px;background:var(--color-background)}[dir=ltr] .k-tabs-dropdown{right:0}[dir=rtl] .k-tabs-dropdown{left:0}.k-tabs-dropdown{top:100%}[dir=ltr] .k-tabs-badge{right:2px}[dir=rtl] .k-tabs-badge{left:2px}.k-tabs-badge{position:absolute;top:3px;font-variant-numeric:tabular-nums;line-height:1.5;padding:0 .25rem;border-radius:2px;font-size:10px;box-shadow:var(--shadow-md)}.k-tabs[data-theme=notice] .k-tabs-badge{background:var(--theme-light);color:var(--color-black)}.k-view{padding-left:1.5rem;padding-right:1.5rem;margin:0 auto;max-width:100rem}@media screen and (min-width:30em){.k-view{padding-left:3rem;padding-right:3rem}}@media screen and (min-width:90em){.k-view{padding-left:6rem;padding-right:6rem}}.k-view[data-align=center]{height:100vh;display:flex;align-items:center;justify-content:center;padding:0 3rem;overflow:auto}.k-view[data-align=center]>*{flex-basis:22.5rem}.k-fatal{position:fixed;top:0;right:0;bottom:0;left:0;background:var(--color-backdrop);display:flex;z-index:var(--z-fatal);align-items:center;justify-content:center;padding:1.5rem}.k-fatal-box{width:100%;height:100%;display:flex;flex-direction:column;color:var(--color-black);background:var(--color-red-400);box-shadow:var(--shadow-xl);border-radius:var(--rounded)}.k-fatal-box .k-headline{line-height:1;font-size:var(--text-sm);padding:.75rem}.k-fatal-box .k-button{padding:.75rem}.k-fatal-iframe{border:0;width:100%;flex-grow:1;background:var(--color-white)}.k-headline{--size:var(--text-base);font-size:var(--size);font-weight:var(--font-bold);line-height:1.5em}.k-headline[data-size=small]{--size:var(--text-sm)}.k-headline[data-size=large]{--size:var(--text-xl);font-weight:var(--font-normal)}@media screen and (min-width:65em){.k-headline[data-size=large]{--size:var(--text-2xl)}}.k-headline[data-size=huge]{--size:var(--text-2xl);line-height:1.15em}@media screen and (min-width:65em){.k-headline[data-size=huge]{--size:var(--text-3xl)}}.k-headline[data-theme]{color:var(--theme)}[dir=ltr] .k-headline abbr{padding-left:.25rem}[dir=rtl] .k-headline abbr{padding-right:.25rem}.k-headline abbr{color:var(--color-gray-500);text-decoration:none}.k-icon{--size:1rem;position:relative;line-height:0;display:flex;align-items:center;justify-content:center;flex-shrink:0;font-size:var(--size)}.k-icon[data-size=medium]{--size:2rem}.k-icon[data-size=large]{--size:3rem}.k-icon svg{width:var(--size);height:var(--size);-moz-transform:scale(1)}.k-icon svg *{fill:currentColor}.k-icon[data-back=black]{color:var(--color-white)}.k-icon[data-back=white]{color:var(--color-gray-900)}.k-icon[data-back=pattern]{color:var(--color-white)}[data-disabled=true] .k-icon[data-back=pattern] svg{opacity:1}.k-icon-emoji{display:block;line-height:1;font-style:normal;font-size:var(--size)}@media only screen and (-webkit-min-device-pixel-ratio:2),not all,only screen and (min-resolution:192dpi),only screen and (min-resolution:2dppx){.k-icon-emoji{font-size:1.25em}}.k-icons{position:absolute;width:0;height:0}.k-image span{position:relative;display:block;line-height:0;padding-bottom:100%}.k-image img{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;-o-object-fit:contain;object-fit:contain}[dir=ltr] .k-image-error{left:50%}[dir=rtl] .k-image-error{right:50%}.k-image-error{position:absolute;top:50%;transform:translate(-50%,-50%);color:var(--color-white);font-size:.9em}.k-image-error svg *{fill:#ffffff4d}.k-image[data-cover=true] img{-o-object-fit:cover;object-fit:cover}.k-image[data-back=black] span{background:var(--color-gray-900)}.k-image[data-back=white] span{background:var(--color-white);color:var(--color-gray-900)}.k-image[data-back=white] .k-image-error{background:var(--color-gray-900);color:var(--color-white)}.k-image[data-back=pattern] span{background:var(--color-gray-800) var(--bg-pattern)}.k-loader{z-index:1}.k-loader-icon{animation:Spin .9s linear infinite}.k-offline-warning{position:fixed;top:0;right:0;bottom:0;left:0;z-index:var(--z-offline);background:var(--color-backdrop);display:flex;align-items:center;justify-content:center;line-height:1}.k-offline-warning p{display:flex;align-items:center;gap:.5rem;background:var(--color-white);box-shadow:var(--shadow);padding:.75rem;border-radius:var(--rounded)}.k-offline-warning p .k-icon{color:var(--color-red-400)}.k-progress{-webkit-appearance:none;width:100%;height:.5rem;border-radius:5rem;background:var(--color-border);overflow:hidden;border:0}.k-progress::-webkit-progress-bar{border:0;background:var(--color-border);height:.5rem;border-radius:20px}.k-progress::-webkit-progress-value{border-radius:inherit;background:var(--color-focus);-webkit-transition:width .3s;transition:width .3s}.k-progress::-moz-progress-bar{border-radius:inherit;background:var(--color-focus);-moz-transition:width .3s;transition:width .3s}[dir=ltr] .k-registration,[dir=ltr] .k-registration p{margin-right:1rem}[dir=rtl] .k-registration,[dir=rtl] .k-registration p{margin-left:1rem}.k-registration{display:flex;align-items:center}.k-registration p{color:var(--color-negative-light);font-size:var(--text-sm);font-weight:600}@media screen and (max-width:90em){.k-registration p{display:none}}.k-registration .k-button{color:var(--color-white)}.k-sort-handle{cursor:move;cursor:grab;cursor:-webkit-grab;color:var(--color-gray-900);justify-content:center;align-items:center;line-height:0;width:2rem;height:2rem;display:flex;will-change:opacity,color;transition:opacity .3s;z-index:1}.k-sort-handle svg{width:1rem;height:1rem}.k-sort-handle:active{cursor:grabbing;cursor:-webkit-grabbing}.k-status-icon svg{width:14px;height:14px}.k-status-icon .k-icon{color:var(--theme-light)}.k-status-icon .k-button-text{color:var(--color-black)}.k-status-icon[data-disabled=true]{opacity:1!important}.k-status-icon[data-disabled=true] .k-icon{color:var(--color-gray-400);opacity:.5}.k-text{line-height:1.5em}[dir=ltr] .k-text ol,[dir=ltr] .k-text ul{margin-left:1rem}[dir=rtl] .k-text ol,[dir=rtl] .k-text ul{margin-right:1rem}.k-text li{list-style:inherit}.k-text p,.k-text>ol,.k-text>ul{margin-bottom:1.5em}.k-text a{text-decoration:underline}.k-text>:last-child{margin-bottom:0}.k-text[data-size=tiny]{font-size:var(--text-xs)}.k-text[data-size=small]{font-size:var(--text-sm)}.k-text[data-size=medium]{font-size:var(--text-base)}.k-text[data-size=large]{font-size:var(--text-xl)}.k-text[data-align]{text-align:var(--align)}.k-text[data-theme=help]{font-size:var(--text-sm);color:var(--color-gray-600);line-height:1.25rem}.k-dialog-body .k-text{word-wrap:break-word}.k-user-info{display:flex;align-items:center;line-height:1;font-size:var(--text-sm)}[dir=ltr] .k-user-info .k-image{margin-right:.75rem}[dir=rtl] .k-user-info .k-image{margin-left:.75rem}.k-user-info .k-image{width:1.5rem}[dir=ltr] .k-user-info .k-icon{margin-right:.75rem}[dir=rtl] .k-user-info .k-icon{margin-left:.75rem}.k-user-info .k-icon{width:1.5rem;height:1.5rem;background:var(--color-black);color:var(--color-white)}.k-breadcrumb{padding-left:.5rem;padding-right:.5rem}.k-breadcrumb-dropdown{height:2.5rem;width:2.5rem;display:flex;align-items:center;justify-content:center}.k-breadcrumb ol{display:none;align-items:center}@media screen and (min-width:30em){.k-breadcrumb ol{display:flex}.k-breadcrumb-dropdown{display:none}}.k-breadcrumb li,.k-breadcrumb-link{display:flex;align-items:center;min-width:0}.k-breadcrumb-link{font-size:var(--text-sm);align-self:stretch;padding-top:.625rem;padding-bottom:.625rem;line-height:1.25rem}.k-breadcrumb li{flex-shrink:3}.k-breadcrumb li:last-child{flex-shrink:1}.k-breadcrumb li:not(:last-child):after{content:"/";padding-left:.5rem;padding-right:.5rem;opacity:.5;flex-shrink:0}.k-breadcrumb li:not(:first-child):not(:last-child){max-width:15vw}[dir=ltr] .k-breadcrumb-icon{margin-right:.5rem}[dir=rtl] .k-breadcrumb-icon{margin-left:.5rem}.k-breadcrumb-icon.k-loader{opacity:.5}.k-breadcrumb-link-text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}button{line-height:inherit;border:0;font-family:var(--font-sans);font-size:1rem;color:currentColor;background:0 0;cursor:pointer}button::-moz-focus-inner{padding:0;border:0}.k-button{display:inline-block;position:relative;font-size:var(--text-sm);transition:color .3s;outline:0}.k-button:focus,.k-button:hover{outline:0}.k-button *{vertical-align:middle}.k-button[data-responsive=true] .k-button-text{display:none}@media screen and (min-width:30em){.k-button[data-responsive=true] .k-button-text{display:inline}}.k-button[data-theme]{color:var(--theme)}.k-button-icon{display:inline-flex;align-items:center;line-height:0}[dir=ltr] .k-button-icon~.k-button-text{padding-left:.5rem}[dir=rtl] .k-button-icon~.k-button-text{padding-right:.5rem}.k-button-text{opacity:.75}.k-button:focus .k-button-text,.k-button:hover .k-button-text{opacity:1}.k-button-text b,.k-button-text span{vertical-align:baseline}.k-button[data-disabled=true]{opacity:.5;pointer-events:none;cursor:default}.k-card-options>.k-button[data-disabled=true]{display:inline-flex}.k-button-group{--padding-x:.75rem;--padding-y:1rem;--line-height:1rem;font-size:0;margin:0 calc(var(--padding-x)*-1)}.k-button-group>.k-dropdown{height:calc(var(--line-height) + (var(--padding-y)*2));display:inline-block}.k-button-group>.k-button,.k-button-group>.k-dropdown>.k-button{padding:var(--padding-y) var(--padding-x);line-height:var(--line-height)}.k-button-group .k-dropdown-content{top:calc(100% + 1px);margin:0 var(--padding-x)}.k-dropdown{position:relative}[dir=ltr] .k-dropdown-content{text-align:left}[dir=rtl] .k-dropdown-content{text-align:right}.k-dropdown-content{position:absolute;top:100%;background:var(--color-black);color:var(--color-white);z-index:var(--z-dropdown);box-shadow:var(--shadow-lg);border-radius:var(--rounded);margin-bottom:6rem}[dir=ltr] .k-dropdown-content[data-align=left]{left:0}[dir=ltr] .k-dropdown-content[data-align=right],[dir=rtl] .k-dropdown-content[data-align=left]{right:0}[dir=rtl] .k-dropdown-content[data-align=right]{left:0}.k-dropdown-content>.k-dropdown-item:first-child{margin-top:.5rem}.k-dropdown-content>.k-dropdown-item:last-child{margin-bottom:.5rem}.k-dropdown-content[data-dropup=true]{top:auto;bottom:100%;margin-bottom:.5rem}.k-dropdown-content hr{border-color:currentColor;opacity:.2;margin:.5rem 1rem}.k-dropdown-content[data-theme=light]{background:var(--color-white);color:var(--color-black)}.k-dropdown-item{white-space:nowrap;line-height:1;display:flex;width:100%;align-items:center;font-size:var(--text-sm);padding:6px 16px}.k-dropdown-item:focus{outline:0;box-shadow:var(--shadow-outline)}[dir=ltr] .k-dropdown-item .k-button-figure{padding-right:.5rem}[dir=rtl] .k-dropdown-item .k-button-figure{padding-left:.5rem}.k-dropdown-item .k-button-figure{text-align:center}.k-link{outline:0}.k-options-dropdown,.k-options-dropdown-toggle{display:flex;justify-content:center;align-items:center;height:38px}.k-options-dropdown-toggle{min-width:38px;padding:0 .75rem}.k-pagination{-webkit-user-select:none;-moz-user-select:none;user-select:none;direction:ltr}.k-pagination .k-button{padding:1rem}.k-pagination-details{white-space:nowrap}.k-pagination>span{font-size:var(--text-sm)}.k-pagination[data-align]{text-align:var(--align)}[dir=ltr] .k-dropdown-content.k-pagination-selector{left:50%}[dir=rtl] .k-dropdown-content.k-pagination-selector{right:50%}.k-dropdown-content.k-pagination-selector{position:absolute;top:100%;transform:translate(-50%);background:var(--color-black)}[dir=ltr] .k-dropdown-content.k-pagination-selector{direction:ltr}[dir=rtl] .k-dropdown-content.k-pagination-selector{direction:rtl}.k-pagination-settings{display:flex;align-items:center;justify-content:space-between}.k-pagination-settings .k-button{line-height:1}[dir=ltr] .k-pagination-settings label{border-right:1px solid rgba(255,255,255,.35)}[dir=rtl] .k-pagination-settings label{border-left:1px solid rgba(255,255,255,.35)}.k-pagination-settings label{display:flex;align-items:center;padding:.625rem 1rem;font-size:var(--text-xs)}[dir=ltr] .k-pagination-settings label span{margin-right:.5rem}[dir=rtl] .k-pagination-settings label span{margin-left:.5rem}.k-prev-next{direction:ltr}.k-search{max-width:30rem;margin:2.5rem auto;box-shadow:var(--shadow-lg);background:var(--color-light);border-radius:var(--rounded)}.k-search-input{display:flex}.k-search-types{flex-shrink:0;display:flex}[dir=ltr] .k-search-types>.k-button{padding-left:1rem}[dir=rtl] .k-search-types>.k-button{padding-right:1rem}.k-search-types>.k-button{font-size:var(--text-base);line-height:1;height:2.5rem}.k-search-types>.k-button .k-icon{height:2.5rem}.k-search-types>.k-button .k-button-text{opacity:1;font-weight:500}.k-search-input input{background:0 0;flex-grow:1;font:inherit;padding:.75rem;border:0;height:2.5rem}.k-search-close{width:3rem;line-height:1}.k-search-close .k-icon-loader{animation:Spin 2s linear infinite}.k-search input:focus{outline:0}.k-search-results{padding:.5rem 1rem 1rem}.k-search .k-item:not(:last-child){margin-bottom:.25rem}.k-search .k-item[data-selected=true]{outline:2px solid var(--color-focus)}.k-search .k-item-info,.k-search-empty{font-size:var(--text-xs)}.k-search-empty{text-align:center;color:var(--color-gray-600)}.k-tag{position:relative;font-size:var(--text-sm);line-height:1;cursor:pointer;background-color:var(--color-gray-900);color:var(--color-light);border-radius:var(--rounded);display:flex;align-items:center;justify-content:space-between;-webkit-user-select:none;-moz-user-select:none;user-select:none}.k-tag:focus{outline:0;background-color:var(--color-focus);color:#fff}.k-tag-text{padding:.3rem .75rem .375rem;line-height:var(--leading-tight)}[dir=ltr] .k-tag-toggle{padding-right:1px}[dir=rtl] .k-tag-toggle{padding-left:1px}[dir=ltr] .k-tag-toggle{border-left:1px solid rgba(255,255,255,.15)}[dir=rtl] .k-tag-toggle{border-right:1px solid rgba(255,255,255,.15)}.k-tag-toggle{color:#ffffffb3;width:1.75rem;height:100%}.k-tag-toggle:hover{background:rgba(255,255,255,.2);color:#fff}[data-disabled=true] .k-tag{background-color:var(--color-gray-600)}[data-disabled=true] .k-tag .k-tag-toggle{display:none}.k-topbar{--bg:var(--color-gray-900);position:relative;color:var(--color-white);flex-shrink:0;height:2.5rem;line-height:1;background:var(--bg)}.k-topbar-wrapper{position:relative;display:flex;align-items:center;margin-left:-.75rem;margin-right:-.75rem}[dir=ltr] .k-topbar-wrapper:after{left:100%}[dir=rtl] .k-topbar-wrapper:after{right:100%}.k-topbar-wrapper:after{position:absolute;content:"";height:2.5rem;background:var(--bg);width:3rem}.k-topbar-menu{flex-shrink:0}.k-topbar-menu ul{padding:.5rem 0}.k-topbar .k-button[data-theme]{color:var(--theme-light)}.k-topbar .k-button-text{opacity:1}.k-topbar-menu-button{display:flex;align-items:center}.k-topbar-menu .k-link[aria-current]{color:var(--color-focus);font-weight:500}.k-topbar-button{padding:.75rem;line-height:1;font-size:var(--text-sm)}.k-topbar-button .k-button-text{display:flex}[dir=ltr] .k-topbar-view-button{padding-right:0}[dir=rtl] .k-topbar-view-button{padding-left:0}.k-topbar-view-button{flex-shrink:0;display:flex;align-items:center}[dir=ltr] .k-topbar-view-button .k-icon{margin-right:.5rem}[dir=rtl] .k-topbar-view-button .k-icon{margin-left:.5rem}[dir=ltr] .k-topbar-signals{right:0}[dir=rtl] .k-topbar-signals{left:0}.k-topbar-signals{position:absolute;top:0;background:var(--bg);height:2.5rem;display:flex;align-items:center}.k-topbar-signals:before{position:absolute;content:"";top:-.5rem;bottom:0;width:.5rem;background:-webkit-linear-gradient(inline-start,rgba(17,17,17,0),#111)}.k-topbar-signals .k-button{line-height:1}.k-topbar-notification{font-weight:var(--font-bold);line-height:1;display:flex}@media screen and (max-width:30em){.k-topbar .k-button[data-theme=negative] .k-button-text{display:none}}.k-section,.k-sections{padding-bottom:3rem}.k-section-header{position:relative;display:flex;align-items:baseline;z-index:1}[dir=ltr] .k-section-header .k-headline{padding-right:var(--spacing-3)}[dir=rtl] .k-section-header .k-headline{padding-left:var(--spacing-3)}.k-section-header .k-headline{line-height:1.25rem;min-height:2rem;flex-grow:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}[dir=ltr] .k-section-header .k-button-group{right:0}[dir=rtl] .k-section-header .k-button-group{left:0}.k-section-header .k-button-group{position:absolute;top:calc(-.5rem - 1px)}.k-section-header .k-button-group>.k-button{padding:.75rem;display:inline-flex}.k-fields-issue-headline{margin-bottom:.5rem}.k-fields-section input[type=submit]{display:none}[data-locked=true] .k-fields-section{opacity:.2;pointer-events:none}.k-models-section[data-processing=true]{pointer-events:none}.k-models-section-search.k-input{margin-bottom:var(--spacing-3);background:var(--color-gray-300);padding:var(--spacing-2) var(--spacing-3);height:var(--field-input-height);border-radius:var(--rounded);font-size:var(--text-sm)}.k-info-section-headline{margin-bottom:.5rem}.k-user-profile{background:var(--color-white)}.k-user-profile>.k-view{padding-top:3rem;padding-bottom:3rem;display:flex;align-items:center;line-height:0}[dir=ltr] .k-user-profile .k-button-group{margin-left:.75rem}[dir=rtl] .k-user-profile .k-button-group{margin-right:.75rem}.k-user-profile .k-button-group{overflow:hidden}.k-user-profile .k-button-group .k-button{display:block;padding-top:.25rem;padding-bottom:.25rem;overflow:hidden;white-space:nowrap}.k-user-view-image .k-icon,.k-user-view-image .k-image{width:5rem;height:5rem;border-radius:var(--rounded);line-height:0;overflow:hidden}.k-user-view-image[data-disabled=true]{opacity:1}.k-user-view-image .k-image{display:block}.k-user-view-image .k-button-text{opacity:1}.k-user-name-placeholder{color:var(--color-gray-500);transition:color .3s}.k-header[data-editable=true] .k-user-name-placeholder:hover{color:var(--color-gray-900)}.k-error-view{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center}.k-error-view-content{line-height:1.5em;max-width:25rem;text-align:center}.k-error-view-icon{color:var(--color-negative);display:inline-block}.k-error-view-content p:not(:last-child){margin-bottom:.75rem}.k-installation-view .k-button{display:block;margin-top:1.5rem}.k-installation-view .k-headline{margin-bottom:.75rem}.k-installation-issues{line-height:1.5em;font-size:var(--text-sm)}[dir=ltr] .k-installation-issues li{padding-left:3.5rem}[dir=rtl] .k-installation-issues li{padding-right:3.5rem}.k-installation-issues li{position:relative;padding:1.5rem;background:var(--color-white)}[dir=ltr] .k-installation-issues .k-icon{left:1.5rem}[dir=rtl] .k-installation-issues .k-icon{right:1.5rem}.k-installation-issues .k-icon{position:absolute;top:calc(1.5rem + 2px)}.k-installation-issues .k-icon svg *{fill:var(--color-negative)}.k-installation-issues li:not(:last-child){margin-bottom:2px}.k-installation-issues li code{font:inherit;color:var(--color-negative)}[dir=ltr] .k-installation-view .k-button[type=submit]{margin-left:-1rem}[dir=rtl] .k-installation-view .k-button[type=submit]{margin-right:-1rem}.k-installation-view .k-button[type=submit]{padding:1rem}.k-languages-view .k-header{margin-bottom:1.5rem}.k-languages-view-section-header{margin-bottom:.5rem}.k-languages-view-section{margin-bottom:3rem}.k-login-fields{position:relative}[dir=ltr] .k-login-toggler{right:0}[dir=rtl] .k-login-toggler{left:0}.k-login-toggler{position:absolute;top:0;z-index:1;text-decoration:underline;font-size:.875rem}.k-login-form label abbr{visibility:hidden}.k-login-buttons{display:flex;align-items:center;justify-content:flex-end;padding:1.5rem 0}[dir=ltr] .k-login-button{margin-right:-1rem}[dir=rtl] .k-login-button{margin-left:-1rem}.k-login-button{padding:.5rem 1rem;font-weight:500;transition:opacity .3s}.k-login-button span{opacity:1}.k-login-button[disabled]{opacity:.25}.k-login-back-button,.k-login-checkbox{display:flex;align-items:center;flex-grow:1}[dir=ltr] .k-login-back-button{margin-left:-1rem}[dir=rtl] .k-login-back-button{margin-right:-1rem}.k-login-checkbox{padding:.5rem 0;font-size:var(--text-sm);cursor:pointer}.k-login-checkbox .k-checkbox-text{opacity:.75;transition:opacity .3s}.k-login-checkbox:focus span,.k-login-checkbox:hover span{opacity:1}.k-password-reset-view .k-user-info{height:38px;margin-bottom:2.25rem;padding:.5rem;background:var(--color-white);border-radius:var(--rounded-xs);box-shadow:var(--shadow)}.k-system-view .k-header{margin-bottom:1.5rem}.k-system-view-section-header{margin-bottom:.5rem;display:flex;justify-content:space-between}.k-system-view-section{margin-bottom:3rem}.k-system-info [data-theme] .k-stat-value{color:var(--theme)}.k-block-type-code-editor{position:relative;font-size:var(--text-sm);line-height:1.5em;background:#000;border-radius:var(--rounded);padding:.5rem .75rem 3rem;color:#fff;font-family:var(--font-mono)}.k-block-type-code-editor .k-editor{white-space:pre-wrap;line-height:1.75em}[dir=ltr] .k-block-type-code-editor-language{right:0}[dir=ltr] .k-block-type-code-editor-language .k-icon,[dir=rtl] .k-block-type-code-editor-language{left:0}.k-block-type-code-editor-language{font-size:var(--text-sm);position:absolute;bottom:0}[dir=rtl] .k-block-type-code-editor-language .k-icon{right:0}.k-block-type-code-editor-language .k-icon{position:absolute;top:0;height:1.5rem;display:flex;width:2rem;z-index:0}.k-block-type-code-editor-language .k-select-input{position:relative;padding:.325rem .75rem .5rem 2rem;z-index:1;font-size:var(--text-xs)}.k-block-type-default .k-block-title{line-height:1.5em}.k-block-type-gallery ul{display:grid;grid-gap:.75rem;grid-template-columns:repeat(auto-fit,minmax(6rem,1fr));line-height:0;align-items:center;justify-content:center;cursor:pointer}.k-block-type-gallery li:empty{padding-bottom:100%;background:var(--color-background)}.k-block-type-gallery li{display:flex;position:relative;align-items:center;justify-content:center}.k-block-type-gallery li img{flex-grow:1;max-width:100%}.k-block-type-heading-input{line-height:1.25em;font-weight:var(--font-bold)}.k-block-type-heading-input[data-level=h1]{font-size:var(--text-3xl);line-height:1.125em}.k-block-type-heading-input[data-level=h2]{font-size:var(--text-2xl)}.k-block-type-heading-input[data-level=h3]{font-size:var(--text-xl)}.k-block-type-heading-input[data-level=h4]{font-size:var(--text-lg)}.k-block-type-heading-input[data-level=h5]{line-height:1.5em;font-size:var(--text-base)}.k-block-type-heading-input[data-level=h6]{line-height:1.5em;font-size:var(--text-sm)}.k-block-type-heading-input .ProseMirror strong{font-weight:700}.k-block-type-image .k-block-figure-container{display:block;text-align:center;line-height:0}.k-block-type-image-auto{max-width:100%;max-height:30rem}.k-block-type-line hr{margin-top:.75rem;margin-bottom:.75rem;border:0;border-top:2px solid var(--color-gray-400)}.k-block-type-markdown-input{position:relative;font-size:var(--text-sm);line-height:1.5em;background:var(--color-background);border-radius:var(--rounded);padding:.5rem .5rem 0;font-family:var(--font-mono)}[dir=ltr] .k-block-type-quote-editor{padding-left:1rem}[dir=rtl] .k-block-type-quote-editor{padding-right:1rem}[dir=ltr] .k-block-type-quote-editor{border-left:2px solid var(--color-black)}[dir=rtl] .k-block-type-quote-editor{border-right:2px solid var(--color-black)}.k-block-type-quote-text{font-size:var(--text-xl);margin-bottom:.25rem;line-height:1.25em}.k-block-type-quote-citation{font-style:italic;font-size:var(--text-sm);color:var(--color-gray-600)}.k-block-type-table-preview{cursor:pointer;border:1px solid var(--color-gray-300);border-spacing:0;border-radius:var(--rounded-sm);overflow:hidden}[dir=ltr] .k-block-type-table-preview td,[dir=ltr] .k-block-type-table-preview th{text-align:left}[dir=rtl] .k-block-type-table-preview td,[dir=rtl] .k-block-type-table-preview th{text-align:right}.k-block-type-table-preview td,.k-block-type-table-preview th{line-height:1.5em;font-size:var(--text-sm)}.k-block-type-table-preview th{padding:.5rem .75rem}.k-block-type-table-preview td:not(.k-table-index-column){padding:0 .75rem}.k-block-type-table-preview td [class$=-field-preview],.k-block-type-table-preview td>*{padding:0}.k-block-type-text-input{font-size:var(--text-base);line-height:1.5em;height:100%}.k-block-container-type-text,.k-block-type-text,.k-block-type-text .k-writer .ProseMirror{height:100%}.k-block-container{position:relative;padding:.75rem;background:var(--color-white);border-radius:var(--rounded)}.k-block-container:not(:last-of-type){border-bottom:1px dashed rgba(0,0,0,.1)}.k-block-container:focus{outline:0}.k-block-container[data-batched=true],.k-block-container[data-selected=true]{z-index:2;border-bottom-color:transparent}.k-block-container[data-batched=true]:after{position:absolute;top:0;right:0;bottom:0;left:0;content:"";background:rgba(238,242,246,.375);mix-blend-mode:multiply;border:1px solid var(--color-focus)}.k-block-container[data-selected=true]{box-shadow:var(--color-focus) 0 0 0 1px,var(--color-focus-outline) 0 0 0 3px}[dir=ltr] .k-block-container .k-block-options{right:.75rem}[dir=rtl] .k-block-container .k-block-options{left:.75rem}.k-block-container .k-block-options{display:none;position:absolute;top:0;margin-top:calc(-1.75rem + 2px)}.k-block-container[data-last-in-batch=true]>.k-block-options,.k-block-container[data-selected=true]>.k-block-options{display:flex}.k-block-container[data-hidden=true] .k-block{opacity:.25}.k-drawer-options .k-button[data-disabled=true]{vertical-align:middle;display:inline-grid}[data-disabled=true] .k-block-container{background:var(--color-background)}.k-block-importer.k-dialog{background:#313740;color:var(--color-white)}.k-block-importer .k-dialog-body{padding:0}.k-block-importer label{display:block;padding:var(--spacing-6) var(--spacing-6)0;color:var(--color-gray-400)}.k-block-importer label kbd{background:rgba(0,0,0,.5);font-family:var(--font-mono);letter-spacing:.1em;padding:.25rem;border-radius:var(--rounded);margin:0 .25rem}.k-block-importer textarea{width:100%;height:20rem;background:0 0;font:inherit;color:var(--color-white);border:0;padding:var(--spacing-6);resize:none}.k-block-importer textarea:focus{outline:0}.k-blocks{background:var(--color-white);box-shadow:var(--shadow);border-radius:var(--rounded)}[data-disabled=true] .k-blocks{background:var(--color-background)}.k-blocks[data-multi-select-key=true] .k-block-container>*{pointer-events:none}.k-blocks[data-empty=true]{padding:0;background:0 0;box-shadow:none}.k-blocks .k-sortable-ghost{outline:2px solid var(--color-focus);box-shadow:#11111140 0 5px 10px;cursor:grabbing;cursor:-webkit-grabbing}.k-blocks-list>.k-blocks-empty{display:flex;align-items:center}.k-blocks-list>.k-blocks-empty:not(:only-child){display:none}.k-block-figure{cursor:pointer}.k-block-figure iframe{border:0;pointer-events:none;background:var(--color-black)}.k-block-figure figcaption{padding-top:.5rem;color:var(--color-gray-600);font-size:var(--text-sm);text-align:center}.k-block-figure-empty.k-button{display:flex;width:100%;height:6rem;border-radius:var(--rounded-sm);align-items:center;justify-content:center;color:var(--color-gray-600);background:var(--color-background)}.k-block-options{display:flex;align-items:center;background:var(--color-white);z-index:var(--z-dropdown);box-shadow:#0000001a -2px 0 5px,var(--shadow),var(--shadow-xl);color:var(--color-black);border-radius:var(--rounded)}[dir=ltr] .k-block-options-button{border-right:1px solid var(--color-background)}[dir=rtl] .k-block-options-button{border-left:1px solid var(--color-background)}.k-block-options-button{--block-options-button-size:30px;width:var(--block-options-button-size);height:var(--block-options-button-size);line-height:1;display:inline-flex;align-items:center;justify-content:center}[dir=ltr] .k-block-options-button:first-child{border-top-left-radius:var(--rounded)}[dir=rtl] .k-block-options-button:first-child{border-top-right-radius:var(--rounded)}[dir=ltr] .k-block-options-button:first-child{border-bottom-left-radius:var(--rounded)}[dir=rtl] .k-block-options-button:first-child{border-bottom-right-radius:var(--rounded)}[dir=ltr] .k-block-options-button:last-child{border-top-right-radius:var(--rounded)}[dir=rtl] .k-block-options-button:last-child{border-top-left-radius:var(--rounded)}[dir=ltr] .k-block-options-button:last-child{border-bottom-right-radius:var(--rounded)}[dir=rtl] .k-block-options-button:last-child{border-bottom-left-radius:var(--rounded)}[dir=ltr] .k-block-options-button:last-of-type{border-right:0}[dir=rtl] .k-block-options-button:last-of-type{border-left:0}.k-block-options-button[aria-current]{color:var(--color-focus)}.k-block-options-button:hover{background:var(--color-gray-100)}.k-block-options .k-dropdown-content{margin-top:.5rem}.k-block-selector.k-dialog{background:var(--color-dark);color:var(--color-white)}.k-block-selector .k-headline{margin-bottom:1rem}.k-block-selector details:not(:last-of-type){margin-bottom:1.5rem}.k-block-selector summary{font-size:var(--text-xs);cursor:pointer;color:var(--color-gray-400)}.k-block-selector details:only-of-type summary{pointer-events:none}.k-block-selector summary:focus{outline:0}.k-block-selector summary:focus-visible{color:var(--color-green-400)}.k-block-types{display:grid;grid-gap:2px;margin-top:.75rem;grid-template-columns:repeat(1,1fr)}[dir=ltr] .k-block-types .k-button{text-align:left}[dir=rtl] .k-block-types .k-button{text-align:right}.k-block-types .k-button{display:flex;align-items:flex-start;border-radius:var(--rounded);background:rgba(0,0,0,.5);width:100%;padding:0 .75rem 0 0;line-height:1.5em}.k-block-types .k-button:focus{outline:2px solid var(--color-green-300)}.k-block-types .k-button .k-button-text{padding:.5rem 0 .5rem .5rem}.k-block-types .k-button .k-icon{width:38px;height:38px}.k-clipboard-hint{padding-top:1.5rem;font-size:var(--text-xs);color:var(--color-gray-400)}.k-clipboard-hint kbd{background:rgba(0,0,0,.5);font-family:var(--font-mono);letter-spacing:.1em;padding:.25rem;border-radius:var(--rounded);margin:0 .25rem}[dir=ltr] .k-block-title{padding-right:.75rem}[dir=rtl] .k-block-title{padding-left:.75rem}.k-block-title{display:flex;align-items:center;min-width:0;font-size:var(--text-sm);line-height:1}[dir=ltr] .k-block-icon{margin-right:.5rem}[dir=rtl] .k-block-icon{margin-left:.5rem}.k-block-icon{width:1rem;color:var(--color-gray-500)}[dir=ltr] .k-block-name{margin-right:.5rem}[dir=rtl] .k-block-name{margin-left:.5rem}.k-block-label{color:var(--color-gray-600);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.k-bubbles-field-preview{padding:.325rem .75rem}.k-text-field-preview{padding:.325rem .75rem;overflow-x:hidden;text-overflow:ellipsis;white-space:nowrap}.k-url-field-preview{padding:.325rem .75rem}.k-url-field-preview a{color:var(--color-focus);text-decoration:underline;transition:color .3s;white-space:nowrap;max-width:100%}.k-url-field-preview a:hover{color:var(--color-black)}.k-flag-field-preview{height:var(--table-row-height);width:var(--table-row-height);display:flex;justify-content:center;align-items:center}.k-html-field-preview{padding:.325rem .75rem;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:1.5em}.k-html-field-preview p:not(:last-child){margin-bottom:1.5em}[dir=ltr] .k-html-field-preview ol,[dir=ltr] .k-html-field-preview ul{margin-left:1rem}[dir=rtl] .k-html-field-preview ol,[dir=rtl] .k-html-field-preview ul{margin-right:1rem}.k-html-field-preview ul>li{list-style:disc}.k-html-field-preview ol ul>li,.k-html-field-preview ul ul>li{list-style:circle}.k-html-field-preview ol>li{list-style:decimal}.k-html-field-preview ol>li::marker{color:var(--color-gray-500);font-size:var(--text-xs)}.k-toggle-field-preview label{padding:0 .25rem 0 .75rem;display:flex;height:38px;cursor:pointer;overflow:hidden;white-space:nowrap}[dir=ltr] .k-toggle-field-preview .k-toggle-input-label{padding-left:.5rem}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input-label,[dir=rtl] .k-toggle-field-preview .k-toggle-input-label{padding-right:.5rem}[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input-label{padding-left:.5rem}[dir=ltr] .k-toggle-field-preview .k-toggle-input{padding-left:.75rem;padding-right:.25rem}.k-toggle-field-preview .k-toggle-input{padding-top:0;padding-bottom:0}[dir=ltr] [data-align=right] .k-toggle-field-preview .k-toggle-input,[dir=rtl] .k-toggle-field-preview .k-toggle-input{padding-left:.25rem;padding-right:.75rem}[dir=rtl] [data-align=right] .k-toggle-field-preview .k-toggle-input{padding-right:.25rem;padding-left:.75rem}[data-align=right] .k-toggle-field-preview .k-toggle-input{flex-direction:row-reverse}[data-align=left]{--align:start}[data-align=center]{--align:center}[data-align=right]{--align:end}[data-invalid=true]{border:1px solid var(--color-negative-outline);box-shadow:var(--color-negative-outline) 0 0 3px 2px}[data-invalid=true]:focus-within{border:var(--field-input-invalid-focus-border)!important;box-shadow:var(--color-negative-outline) 0 0 0 2px!important}[data-tabbed=true]{box-shadow:var(--shadow-outline);border-radius:var(--rounded)}[data-theme=positive],[data-theme=success]{--theme:var(--color-positive);--theme-light:var(--color-positive-light);--theme-bg:var(--color-green-300)}[data-theme=error],[data-theme=negative]{--theme:var(--color-negative);--theme-light:var(--color-negative-light);--theme-bg:var(--color-red-300)}[data-theme=notice]{--theme:var(--color-notice);--theme-light:var(--color-notice-light);--theme-bg:var(--color-orange-300)}[data-theme=info]{--theme:var(--color-focus);--theme-light:var(--color-focus-light);--theme-bg:var(--color-blue-200)}.scroll-x,.scroll-x-auto,.scroll-y,.scroll-y-auto{-webkit-overflow-scrolling:touch;transform:translateZ(0)}.scroll-x,.scroll-x-auto{overflow-x:scroll;overflow-y:hidden}.scroll-x-auto{overflow-x:auto}.scroll-y,.scroll-y-auto{overflow-x:hidden;overflow-y:scroll}.scroll-y-auto{overflow-y:auto}.input-hidden{position:absolute;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:0;height:0;opacity:0}.k-offscreen,.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0} diff --git a/kirby/panel/dist/img/icons.svg b/kirby/panel/dist/img/icons.svg old mode 100755 new mode 100644 index 7bcd793..d0eba91 --- a/kirby/panel/dist/img/icons.svg +++ b/kirby/panel/dist/img/icons.svg @@ -24,12 +24,18 @@ + + + + + + @@ -60,6 +66,10 @@ + + + + @@ -140,6 +150,15 @@ + + + + + + + + + @@ -175,69 +194,35 @@ - - - - - - - - + + - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - + - - - - - - + + + + - - - - + - - - + + + + + + + + + + + @@ -248,6 +233,11 @@ + + + + + @@ -255,6 +245,9 @@ + + + @@ -422,6 +415,10 @@ + + + + @@ -456,6 +453,9 @@ + + + @@ -466,6 +466,9 @@ + + + @@ -573,6 +576,9 @@ + + + @@ -626,6 +632,9 @@ + + + diff --git a/kirby/panel/dist/js/index.js b/kirby/panel/dist/js/index.js index 1fdf2c6..5600308 100644 --- a/kirby/panel/dist/js/index.js +++ b/kirby/panel/dist/js/index.js @@ -1 +1 @@ -var t=Object.defineProperty,e=Object.defineProperties,n=Object.getOwnPropertyDescriptors,s=Object.getOwnPropertySymbols,i=Object.prototype.hasOwnProperty,o=Object.prototype.propertyIsEnumerable,r=(e,n,s)=>n in e?t(e,n,{enumerable:!0,configurable:!0,writable:!0,value:s}):e[n]=s,a=(t,e)=>{for(var n in e||(e={}))i.call(e,n)&&r(t,n,e[n]);if(s)for(var n of s(e))o.call(e,n)&&r(t,n,e[n]);return t},l=(t,s)=>e(t,n(s));import{V as u,a as c,m as d,d as p,c as h,b as f,I as m,P as g,S as k,F as v,N as b,s as y,l as $,w as _,e as w,f as x,t as S,g as C,h as O,i as E,j as T,k as A,n as M,D as I,o as L,E as j,p as D,q as B,r as P,T as N,u as q,v as R,x as F,y as z,z as Y,A as H,B as U,C as K,G as J,H as G,J as V}from"./vendor.js";var W=t=>({changeName:async(e,n,s)=>t.patch(e+"/files/"+n+"/name",{name:s}),delete:async(e,n)=>t.delete(e+"/files/"+n),async get(e,n,s){let i=await t.get(e+"/files/"+n,s);return!0===Array.isArray(i.content)&&(i.content={}),i},link(t,e,n){return"/"+this.url(t,e,n)},update:async(e,n,s)=>t.patch(e+"/files/"+n,s),url(t,e,n){let s=t+"/files/"+e;return n&&(s+="/"+n),s}}),X=t=>({async blueprint(e){return t.get("pages/"+this.id(e)+"/blueprint")},async blueprints(e,n){return t.get("pages/"+this.id(e)+"/blueprints",{section:n})},async changeSlug(e,n){return t.patch("pages/"+this.id(e)+"/slug",{slug:n})},async changeStatus(e,n,s){return t.patch("pages/"+this.id(e)+"/status",{status:n,position:s})},async changeTemplate(e,n){return t.patch("pages/"+this.id(e)+"/template",{template:n})},async changeTitle(e,n){return t.patch("pages/"+this.id(e)+"/title",{title:n})},async children(e,n){return t.post("pages/"+this.id(e)+"/children/search",n)},async create(e,n){return null===e||"/"===e?t.post("site/children",n):t.post("pages/"+this.id(e)+"/children",n)},async delete(e,n){return t.delete("pages/"+this.id(e),n)},async duplicate(e,n,s){return t.post("pages/"+this.id(e)+"/duplicate",{slug:n,children:s.children||!1,files:s.files||!1})},async get(e,n){let s=await t.get("pages/"+this.id(e),n);return!0===Array.isArray(s.content)&&(s.content={}),s},id:t=>t.replace(/\//g,"+"),async files(e,n){return t.post("pages/"+this.id(e)+"/files/search",n)},link(t){return"/"+this.url(t)},async preview(t){return(await this.get(this.id(t),{select:"previewUrl"})).previewUrl},async search(e,n){return e?t.post("pages/"+this.id(e)+"/children/search?select=id,title,hasChildren",n):t.post("site/children/search?select=id,title,hasChildren",n)},async update(e,n){return t.patch("pages/"+this.id(e),n)},url(t,e){let n=null===t?"pages":"pages/"+String(t).replace(/\//g,"+");return e&&(n+="/"+e),n}});var Z=t=>({running:0,async request(e,n,s=!1){n=Object.assign(n||{},{credentials:"same-origin",cache:"no-store",headers:a({"x-requested-with":"xmlhttprequest","content-type":"application/json"},n.headers)}),t.methodOverwrite&&"GET"!==n.method&&"POST"!==n.method&&(n.headers["x-http-method-override"]=n.method,n.method="POST"),n=t.onPrepare(n);const i=e+"/"+JSON.stringify(n);t.onStart(i,s),this.running++;const o=await fetch([t.endpoint,e].join(t.endpoint.endsWith("/")||e.startsWith("/")?"":"/"),n);try{const e=await async function(t){const e=await t.text();let n;try{n=JSON.parse(e)}catch(s){return window.panel.$vue.$api.onParserError({html:e}),!1}return n}(o);if(o.status<200||o.status>299)throw e;if("error"===e.status)throw e;let n=e;return e.data&&"model"===e.type&&(n=e.data),this.running--,t.onComplete(i),t.onSuccess(e),n}catch(r){throw this.running--,t.onComplete(i),t.onError(r),r}},async get(t,e,n,s=!1){return e&&(t+="?"+Object.keys(e).filter((t=>void 0!==e[t]&&null!==e[t])).map((t=>t+"="+e[t])).join("&")),this.request(t,Object.assign(n||{},{method:"GET"}),s)},async post(t,e,n,s="POST",i=!1){return this.request(t,Object.assign(n||{},{method:s,body:JSON.stringify(e)}),i)},async patch(t,e,n,s=!1){return this.post(t,e,n,"PATCH",s)},async delete(t,e,n,s=!1){return this.post(t,e,n,"DELETE",s)}}),Q=t=>({blueprint:async e=>t.get("users/"+e+"/blueprint"),blueprints:async(e,n)=>t.get("users/"+e+"/blueprints",{section:n}),changeEmail:async(e,n)=>t.patch("users/"+e+"/email",{email:n}),changeLanguage:async(e,n)=>t.patch("users/"+e+"/language",{language:n}),changeName:async(e,n)=>t.patch("users/"+e+"/name",{name:n}),changePassword:async(e,n)=>t.patch("users/"+e+"/password",{password:n}),changeRole:async(e,n)=>t.patch("users/"+e+"/role",{role:n}),create:async e=>t.post("users",e),delete:async e=>t.delete("users/"+e),deleteAvatar:async e=>t.delete("users/"+e+"/avatar"),link(t,e){return"/"+this.url(t,e)},async list(e){return t.post(this.url(null,"search"),e)},get:async(e,n)=>t.get("users/"+e,n),async roles(e){return(await t.get(this.url(e,"roles"))).data.map((t=>({info:t.description||`(${window.panel.$t("role.description.placeholder")})`,text:t.title,value:t.name})))},search:async e=>t.post("users/search",e),update:async(e,n)=>t.patch("users/"+e,n),url(t,e){let n=t?"users/"+t:"users";return e&&(n+="/"+e),n}}),tt={install(t,e){t.prototype.$api=t.$api=((t={})=>{const e=a(a({},{endpoint:"/api",methodOverwrite:!0,onPrepare:t=>t,onStart(){},onComplete(){},onSuccess(){},onParserError(){},onError(t){throw window.console.log(t.message),t}}),t.config||{});let n=a(a(a({},e),Z(e)),t);return n.auth=(t=>({async login(e){const n={long:e.remember||!1,email:e.email,password:e.password};return t.post("auth/login",n)},logout:async()=>t.post("auth/logout"),user:async e=>t.get("auth",e),verifyCode:async e=>t.post("auth/code",{code:e})}))(n),n.files=W(n),n.languages=(t=>({create:async e=>t.post("languages",e),delete:async e=>t.delete("languages/"+e),get:async e=>t.get("languages/"+e),list:async()=>t.get("languages"),update:async(e,n)=>t.patch("languages/"+e,n)}))(n),n.pages=X(n),n.roles=(t=>({list:async e=>t.get("roles",e),get:async e=>t.get("roles/"+e)}))(n),n.system=(t=>({get:async(e={view:"panel"})=>t.get("system",e),install:async e=>(await t.post("system/install",e)).user,register:async e=>t.post("system/register",e)}))(n),n.site=(t=>({blueprint:async()=>t.get("site/blueprint"),blueprints:async()=>t.get("site/blueprints"),changeTitle:async e=>t.patch("site/title",{title:e}),children:async e=>t.post("site/children/search",e),get:async(e={view:"panel"})=>t.get("site",e),update:async e=>t.post("site",e)}))(n),n.translations=(t=>({list:async()=>t.get("translations"),get:async e=>t.get("translations/"+e)}))(n),n.users=Q(n),n.files.rename=n.files.changeName,n.pages.slug=n.pages.changeSlug,n.pages.status=n.pages.changeStatus,n.pages.template=n.pages.changeTemplate,n.pages.title=n.pages.changeTitle,n.site.title=n.site.changeTitle,n.system.info=n.system.get,n})({config:{endpoint:window.panel.$urls.api,onComplete:n=>{t.$api.requests=t.$api.requests.filter((t=>t!==n)),0===t.$api.requests.length&&e.dispatch("isLoading",!1)},onError:e=>{window.panel.$config.debug&&window.console.error(e),403!==e.code||"Unauthenticated"!==e.message&&"access.panel"!==e.key||t.prototype.$go("/logout")},onParserError:({html:t,silent:n})=>{e.dispatch("fatal",{html:t,silent:n})},onPrepare:t=>(window.panel.$language&&(t.headers["x-language"]=window.panel.$language.code),t.headers["x-csrf"]=window.panel.$system.csrf,t),onStart:(n,s=!1)=>{!1===s&&e.dispatch("isLoading",!0),t.$api.requests.push(n)},onSuccess:()=>{clearInterval(t.$api.ping),t.$api.ping=setInterval(t.$api.auth.user,3e5)}},ping:null,requests:[]}),t.$api.ping=setInterval(t.$api.auth.user,3e5)}},et={name:"Fiber",data:()=>({component:null,state:window.fiber,key:null}),created(){this.$fiber.init(this.state,{base:document.querySelector("base").href,headers:()=>({"X-CSRF":this.state.$system.csrf}),onFatal({text:t,options:e}){this.$store.dispatch("fatal",{html:t,silent:e.silent})},onFinish:()=>{0===this.$api.requests.length&&this.$store.dispatch("isLoading",!1)},onPushState:t=>{window.history.pushState(t,"",t.$url)},onReplaceState:t=>{window.history.replaceState(t,"",t.$url)},onStart:({silent:t})=>{!0!==t&&this.$store.dispatch("isLoading",!0)},onSwap:async(t,e)=>{e=a({navigate:!0,replace:!1},e),this.setGlobals(t),this.setTitle(t),this.setTranslation(t),this.component=t.$view.component,this.state=t,this.key=!0===e.replace?this.key:t.$view.timestamp,!0===e.navigate&&this.navigate()},query:()=>{var t;return{language:null==(t=this.state.$language)?void 0:t.code}}}),window.addEventListener("popstate",this.$reload)},methods:{navigate(){this.$store.dispatch("navigate")},setGlobals(t){["$config","$direction","$language","$languages","$license","$menu","$multilang","$permissions","$searches","$system","$translation","$urls","$user","$view"].forEach((e=>{void 0!==t[e]?u.prototype[e]=window.panel[e]=t[e]:u.prototype[e]=t[e]=window.panel[e]}))},setTitle(t){t.$view.title?document.title=t.$view.title+" | "+t.$system.title:document.title=t.$system.title},setTranslation(t){t.$translation&&(document.documentElement.lang=t.$translation.code)}},render(t){if(this.component)return t(this.component,{key:this.key,props:this.state.$view.props})}};function nt(t){if(void 0!==t)return JSON.parse(JSON.stringify(t))}function st(t,e){for(const n of Object.keys(e))e[n]instanceof Object&&Object.assign(e[n],st(t[n]||{},e[n]));return Object.assign(t||{},e),t}var it={clone:nt,merge:st};const ot=(t,e)=>{localStorage.setItem("kirby$content$"+t,JSON.stringify(e))};var rt={namespaced:!0,state:{current:null,models:{},status:{enabled:!0}},getters:{exists:t=>e=>Object.prototype.hasOwnProperty.call(t.models,e),hasChanges:(t,e)=>t=>{const n=e.model(t).changes;return Object.keys(n).length>0},isCurrent:t=>e=>t.current===e,id:t=>e=>(e=e||t.current,window.panel.$language?e+"?language="+window.panel.$language.code:e),model:(t,e)=>n=>(n=n||t.current,!0===e.exists(n)?t.models[n]:{api:null,originals:{},values:{},changes:{}}),originals:(t,e)=>t=>nt(e.model(t).originals),values:(t,e)=>t=>a(a({},e.originals(t)),e.changes(t)),changes:(t,e)=>t=>nt(e.model(t).changes)},mutations:{CLEAR(t){Object.keys(t.models).forEach((e=>{t.models[e].changes={}})),Object.keys(localStorage).forEach((t=>{t.startsWith("kirby$content$")&&localStorage.removeItem(t)}))},CREATE(t,[e,n]){if(!n)return!1;let s=t.models[e]?t.models[e].changes:n.changes;u.set(t.models,e,{api:n.api,originals:n.originals,changes:s||{}})},CURRENT(t,e){t.current=e},MOVE(t,[e,n]){const s=nt(t.models[e]);u.delete(t.models,e),u.set(t.models,n,s);const i=localStorage.getItem("kirby$content$"+e);localStorage.removeItem("kirby$content$"+e),localStorage.setItem("kirby$content$"+n,i)},REMOVE(t,e){u.delete(t.models,e),localStorage.removeItem("kirby$content$"+e)},REVERT(t,e){t.models[e]&&(u.set(t.models[e],"changes",{}),localStorage.removeItem("kirby$content$"+e))},STATUS(t,e){u.set(t.status,"enabled",e)},UPDATE(t,[e,n,s]){if(!t.models[e])return!1;void 0===s&&(s=null),s=nt(s);const i=JSON.stringify(s);JSON.stringify(t.models[e].originals[n])==i?u.delete(t.models[e].changes,n):u.set(t.models[e].changes,n,s),ot(e,{api:t.models[e].api,originals:t.models[e].originals,changes:t.models[e].changes})}},actions:{init(t){Object.keys(localStorage).filter((t=>t.startsWith("kirby$content$"))).map((t=>t.split("kirby$content$")[1])).forEach((e=>{const n=localStorage.getItem("kirby$content$"+e);t.commit("CREATE",[e,JSON.parse(n)])})),Object.keys(localStorage).filter((t=>t.startsWith("kirby$form$"))).map((t=>t.split("kirby$form$")[1])).forEach((e=>{const n=localStorage.getItem("kirby$form$"+e);let s=null;try{s=JSON.parse(n)}catch(o){}if(!s||!s.api)return localStorage.removeItem("kirby$form$"+e),!1;const i={api:s.api,originals:s.originals,changes:s.values};t.commit("CREATE",[e,i]),ot(e,i),localStorage.removeItem("kirby$form$"+e)}))},clear(t){t.commit("CLEAR")},create(t,e){e.id=t.getters.id(e.id),(e.id.startsWith("/pages/")||e.id.startsWith("/site"))&&delete e.content.title;const n={api:e.api,originals:nt(e.content),changes:{}};t.commit("CREATE",[e.id,n]),t.dispatch("current",e.id)},current(t,e){t.commit("CURRENT",e)},disable(t){t.commit("STATUS",!1)},enable(t){t.commit("STATUS",!0)},move(t,[e,n]){e=t.getters.id(e),n=t.getters.id(n),t.commit("MOVE",[e,n])},remove(t,e){t.commit("REMOVE",e),t.getters.isCurrent(e)&&t.commit("CURRENT",null)},revert(t,e){e=e||t.state.current,t.commit("REVERT",e)},async save(t,e){if(e=e||t.state.current,t.getters.isCurrent(e)&&!1===t.state.status.enabled)return!1;t.dispatch("disable");const n=t.getters.model(e),s=a(a({},n.originals),n.changes);try{await u.$api.patch(n.api,s),t.commit("CREATE",[e,l(a({},n),{originals:s})]),t.dispatch("revert",e)}finally{t.dispatch("enable")}},update(t,[e,n,s]){s=s||t.state.current,t.commit("UPDATE",[s,e,n])}}},at={namespaced:!0,state:{open:[]},mutations:{CLOSE(t,e){t.open=e?t.open.filter((t=>t.id!==e)):[]},GOTO(t,e){t.open=t.open.slice(0,t.open.findIndex((t=>t.id===e))+1)},OPEN(t,e){t.open.push(e)}},actions:{close(t,e){t.commit("CLOSE",e)},goto(t,e){t.commit("GOTO",e)},open(t,e){t.commit("OPEN",e)}}},lt={timer:null,namespaced:!0,state:{type:null,message:null,details:null,timeout:null},mutations:{SET(t,e){t.type=e.type,t.message=e.message,t.details=e.details,t.timeout=e.timeout},UNSET(t){t.type=null,t.message=null,t.details=null,t.timeout=null}},actions:{close(t){clearTimeout(this.timer),t.commit("UNSET")},deprecated(t,e){console.warn("Deprecated: "+e)},error(t,e){let n=e;"string"==typeof e&&(n={message:e}),e instanceof Error&&(n={message:e.message},window.panel.$config.debug&&window.console.error(e)),t.dispatch("dialog",{component:"k-error-dialog",props:n},{root:!0}),t.dispatch("close")},open(t,e){t.dispatch("close"),t.commit("SET",e),e.timeout&&(this.timer=setTimeout((()=>{t.dispatch("close")}),e.timeout))},success(t,e){"string"==typeof e&&(e={message:e}),t.dispatch("open",a({type:"success",timeout:4e3},e))}}};u.use(c);var ut=new c.Store({strict:!1,state:{dialog:null,drag:null,fatal:!1,isLoading:!1},mutations:{SET_DIALOG(t,e){t.dialog=e},SET_DRAG(t,e){t.drag=e},SET_FATAL(t,e){t.fatal=e},SET_LOADING(t,e){t.isLoading=e}},actions:{dialog(t,e){t.commit("SET_DIALOG",e)},drag(t,e){t.commit("SET_DRAG",e)},fatal(t,e){!1!==e?(console.error("The JSON response could not be parsed"),window.panel.$config.debug&&console.info(e.html),e.silent||t.commit("SET_FATAL",e.html)):t.commit("SET_FATAL",!1)},isLoading(t,e){t.commit("SET_LOADING",!0===e)},navigate(t){t.dispatch("dialog",null),t.dispatch("drawers/close")}},modules:{content:rt,drawers:at,notification:lt}}),ct={install(t){window.panel=window.panel||{},window.onunhandledrejection=t=>{t.preventDefault(),ut.dispatch("notification/error",t.reason)},window.panel.deprecated=t=>{ut.dispatch("notification/deprecated",t)},window.panel.error=t.config.errorHandler=t=>{ut.dispatch("notification/error",t)}}},dt={install(t){const e=d(),n={$on:e.on,$off:e.off,$emit:e.emit,click(t){n.$emit("click",t)},copy(t){n.$emit("copy",t)},dragenter(t){n.entered=t.target,n.prevent(t),n.$emit("dragenter",t)},dragleave(t){n.prevent(t),n.entered===t.target&&n.$emit("dragleave",t)},drop(t){n.prevent(t),n.$emit("drop",t)},entered:null,focus(t){n.$emit("focus",t)},keydown(e){let s=["keydown"];(e.metaKey||e.ctrlKey)&&s.push("cmd"),!0===e.altKey&&s.push("alt"),!0===e.shiftKey&&s.push("shift");let i=t.prototype.$helper.string.lcfirst(e.key);const o={escape:"esc",arrowUp:"up",arrowDown:"down",arrowLeft:"left",arrowRight:"right"};o[i]&&(i=o[i]),!1===["alt","control","shift","meta"].includes(i)&&s.push(i),n.$emit(s.join("."),e),n.$emit("keydown",e)},keyup(t){n.$emit("keyup",t)},online(t){n.$emit("online",t)},offline(t){n.$emit("offline",t)},paste(t){n.$emit("paste",t)},prevent(t){t.stopPropagation(),t.preventDefault()}};window.addEventListener("online",n.online),window.addEventListener("offline",n.offline),window.addEventListener("dragenter",n.dragenter,!1),window.addEventListener("dragover",n.prevent,!1),window.addEventListener("dragexit",n.prevent,!1),window.addEventListener("dragleave",n.dragleave,!1),window.addEventListener("drop",n.drop,!1),window.addEventListener("keydown",n.keydown,!1),window.addEventListener("keyup",n.keyup,!1),document.addEventListener("click",n.click,!1),document.addEventListener("copy",n.copy,!0),document.addEventListener("focus",n.focus,!0),document.addEventListener("paste",n.paste,!0),t.prototype.$events=n}};class pt{constructor(t={}){this.options=a({base:"/",headers:()=>({}),onFatal:()=>{},onFinish:()=>{},onPushState:()=>{},onReplaceState:()=>{},onStart:()=>{},onSwap:()=>{},query:()=>({})},t),this.state={}}init(t={},e={}){this.options=a(a({},this.options),e),this.setState(t)}arrayToString(t){return!1===Array.isArray(t)?String(t):t.join(",")}body(t){return"object"==typeof t?JSON.stringify(t):t}async fetch(t,e){return fetch(t,e)}async go(t,e){try{const n=await this.request(t,e);return!1!==n&&this.setState(n,e)}catch(n){if(!0!==(null==e?void 0:e.silent))throw n}}query(t={},e={}){let n=new URLSearchParams(e);return"object"!=typeof t&&(t={}),Object.entries(t).forEach((([t,e])=>{null!==e&&n.set(t,e)})),Object.entries(this.options.query()).forEach((([t,e])=>{var s,i;null!==(e=null!=(i=null!=(s=n.get(t))?s:e)?i:null)&&n.set(t,e)})),n}redirect(t){window.location.href=t}reload(t={}){return this.go(window.location.href,l(a({},t),{replace:!0}))}async request(t="",e={}){var n;const s=!!(e=a({globals:!1,method:"GET",only:[],query:{},silent:!1,type:"$view"},e)).globals&&this.arrayToString(e.globals),i=this.arrayToString(e.only);this.options.onStart(e);try{const r=this.url(t,e.query),u=await this.fetch(r,{method:e.method,body:this.body(e.body),credentials:"same-origin",cache:"no-store",headers:a(l(a({},this.options.headers()),{"X-Fiber":!0,"X-Fiber-Globals":s,"X-Fiber-Only":i,"X-Fiber-Referrer":(null==(n=this.state.$view)?void 0:n.path)||null}),e.headers)});if(!1===u.headers.has("X-Fiber"))return this.redirect(u.url),!1;const c=await u.text();let d;try{d=JSON.parse(c)}catch(o){return this.options.onFatal({url:r,path:t,options:e,response:u,text:c}),!1}if(!d[e.type])throw Error(`The ${e.type} could not be loaded`);const p=d[e.type];if(p.error)throw Error(p.error);return"$view"===e.type?i.length?st(this.state,d):d:p}finally{this.options.onFinish(e)}}async setState(t,e={}){return"object"==typeof t&&(this.state=nt(t),!0===e.replace||this.url(this.state.$url).href===window.location.href?this.options.onReplaceState(this.state,e):this.options.onPushState(this.state,e),this.options.onSwap(this.state,e),this.state)}url(t="",e={}){return(t="string"==typeof t&&null===t.match(/^https?:\/\//)?new URL(this.options.base+t.replace(/^\//,"")):new URL(t)).search=this.query(e,t.search),t}}const ht=async function(t){return a({cancel:null,submit:null,props:{}},t)},ft=async function(t,e={}){let n=null,s=null;"function"==typeof e?(n=e,e={}):(n=e.submit,s=e.cancel);let i=await this.$fiber.request("dialogs/"+t,l(a({},e),{type:"$dialog"}));return"object"==typeof i&&(i.submit=n||null,i.cancel=s||null,i)};async function mt(t,e={}){let n=null;if(n="object"==typeof t?await ht.call(this,t):await ft.call(this,t,e),!n)return!1;if(!n.component||!1===this.$helper.isComponent(n.component))throw Error("The dialog component does not exist");return n.props=n.props||{},this.$store.dispatch("dialog",n),n}function gt(t,e={}){return async n=>{const s=await this.$fiber.request("dropdowns/"+t,l(a({},e),{type:"$dropdown"}));if(!s)return!1;if(!1===Array.isArray(s.options)||0===s.options.length)throw Error("The dropdown is empty");s.options.map((t=>(t.dialog&&(t.click=()=>{const e="string"==typeof t.dialog?t.dialog:t.dialog.url,n="object"==typeof t.dialog?t.dialog:{};return this.$dialog(e,n)}),t))),n(s.options)}}async function kt(t,e,n={}){return await this.$fiber.request("search/"+t,a({query:{query:e},type:"$search"},n))}var vt={install(t){const e=new pt;t.prototype.$fiber=window.panel.$fiber=e,t.prototype.$dialog=window.panel.$dialog=mt,t.prototype.$dropdown=window.panel.$dropdown=gt,t.prototype.$go=window.panel.$go=e.go.bind(e),t.prototype.$reload=window.panel.$reload=e.reload.bind(e),t.prototype.$request=window.panel.$request=e.request.bind(e),t.prototype.$search=window.panel.$search=kt,t.prototype.$url=window.panel.$url=e.url.bind(e)}};var bt={read:function(t){if(!t)return null;if("string"==typeof t)return t;if(t instanceof ClipboardEvent){t.preventDefault();const e=t.clipboardData.getData("text/html")||t.clipboardData.getData("text/plain")||null;if(e)return e.replace(/\u00a0/g," ")}return null},write:function(t,e){if("string"!=typeof t&&(t=JSON.stringify(t,null,2)),e&&e instanceof ClipboardEvent)return e.preventDefault(),e.clipboardData.setData("text/plain",t),!0;const n=document.createElement("textarea");if(n.value=t,document.body.append(n),navigator.userAgent.match(/ipad|ipod|iphone/i)){n.contentEditable=!0,n.readOnly=!0;const t=document.createRange();t.selectNodeContents(n);const e=window.getSelection();e.removeAllRanges(),e.addRange(t),n.setSelectionRange(0,999999)}else n.select();return document.execCommand("copy"),n.remove(),!0}};function yt(t){if("string"==typeof t){if("pattern"===(t=t.toLowerCase()))return"var(--color-gray-800) var(--bg-pattern)";if(!1===t.startsWith("#")&&!1===t.startsWith("var(")){const e="--color-"+t;if(window.getComputedStyle(document.documentElement).getPropertyValue(e))return`var(${e})`}return t}}var $t=(t,e)=>{let n=null;return function(){clearTimeout(n),n=setTimeout((()=>t.apply(this,arguments)),e)}};function _t(t,e=!1){if(!t.match("youtu"))return!1;let n=null;try{n=new URL(t)}catch(d){return!1}const s=n.pathname.split("/").filter((t=>""!==t)),i=s[0],o=s[1],r="https://"+(!0===e?"www.youtube-nocookie.com":n.host)+"/embed",a=t=>!!t&&null!==t.match(/^[a-zA-Z0-9_-]+$/);let l=n.searchParams,u=null;switch(s.join("/")){case"embed/videoseries":case"playlist":a(l.get("list"))&&(u=r+"/videoseries");break;case"watch":a(l.get("v"))&&(u=r+"/"+l.get("v"),l.has("t")&&l.set("start",l.get("t")),l.delete("v"),l.delete("t"));break;default:n.host.includes("youtu.be")&&a(i)?(u="https://www.youtube.com/embed/"+i,l.has("t")&&l.set("start",l.get("t")),l.delete("t")):"embed"===i&&a(o)&&(u=r+"/"+o)}if(!u)return!1;const c=l.toString();return c.length&&(u+="?"+c),u}function wt(t,e=!1){let n=null;try{n=new URL(t)}catch(l){return!1}const s=n.pathname.split("/").filter((t=>""!==t));let i=n.searchParams,o=null;switch(!0===e&&i.append("dnt",1),n.host){case"vimeo.com":case"www.vimeo.com":o=s[0];break;case"player.vimeo.com":o=s[1]}if(!o||!o.match(/^[0-9]*$/))return!1;let r="https://player.vimeo.com/video/"+o;const a=i.toString();return a.length&&(r+="?"+a),r}var xt={youtube:_t,vimeo:wt,video:function(t,e=!1){return t.includes("youtu")?_t(t,e):!!t.includes("vimeo")&&wt(t,e)}},St=t=>void 0!==u.options.components[t],Ct=t=>!!t.dataTransfer&&(!!t.dataTransfer.types&&(!0===t.dataTransfer.types.includes("Files")&&!1===t.dataTransfer.types.includes("text/plain")));var Ot={metaKey:function(){return window.navigator.userAgent.indexOf("Mac")>-1?"cmd":"ctrl"}},Et=(t="3/2",e="100%",n=!0)=>{const s=String(t).split("/");if(2!==s.length)return e;const i=Number(s[0]),o=Number(s[1]);let r=100;return 0!==i&&0!==o&&(r=n?r/i*o:r/o*i,r=parseFloat(String(r)).toFixed(2)),r+"%"},Tt=t=>{var e=(t=t||{}).desc?-1:1,n=-e,s=/^0/,i=/\s+/g,o=/^\s+|\s+$/g,r=/[^\x00-\x80]/,a=/^0x[0-9a-f]+$/i,l=/(0x[\da-fA-F]+|(^[\+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|\d+)/g,u=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,c=t.insensitive?function(t){return function(t){if(t.toLocaleLowerCase)return t.toLocaleLowerCase();return t.toLowerCase()}(""+t).replace(o,"")}:function(t){return(""+t).replace(o,"")};function d(t){return t.replace(l,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0")}function p(t,e){return(!t.match(s)||1===e)&&parseFloat(t)||t.replace(i," ").replace(o,"")||0}return function(t,s){var i=c(t),o=c(s);if(!i&&!o)return 0;if(!i&&o)return n;if(i&&!o)return e;var l=d(i),h=d(o),f=parseInt(i.match(a),16)||1!==l.length&&Date.parse(i),m=parseInt(o.match(a),16)||f&&o.match(u)&&Date.parse(o)||null;if(m){if(fm)return e}for(var g=l.length,k=h.length,v=0,b=Math.max(g,k);v0)return e;if(_<0)return n;if(v===b-1)return 0}else{if(y<$)return n;if(y>$)return e}}return 0}};function At(t){const e={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};return String(t).replace(/[&<>"'`=/]/g,(t=>e[t]))}function Mt(t,e={}){const n=(t,e={})=>{var s;const i=At(t.shift()),o=null!=(s=e[i])?s:null;return null===o?Object.prototype.hasOwnProperty.call(e,i)||"…":0===t.length?o:n(t,o)},s="[{]{1,2}[\\s]?",i="[\\s]?[}]{1,2}";return(t=t.replace(new RegExp(`${s}(.*?)${i}`,"gi"),((t,s)=>n(s.split("."),e)))).replace(new RegExp(`${s}.*${i}`,"gi"),"…")}function It(t){const e=String(t);return e.charAt(0).toUpperCase()+e.slice(1)}RegExp.escape=function(t){return t.replace(new RegExp("[-/\\\\^$*+?.()[\\]{}]","gu"),"\\$&")};var Lt={camelToKebab:function(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()},escapeHTML:At,hasEmoji:function(t){if("string"!=typeof t)return!1;const e=t.match(/(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/i);return null!==e&&null!==e.length},lcfirst:function(t){const e=String(t);return e.charAt(0).toLowerCase()+e.slice(1)},pad:function(t,e=2){t=String(t);let n="";for(;n.length]+)>)/gi,"")},template:Mt,ucfirst:It,ucwords:function(t){return String(t).split(/ /g).map((t=>It(t))).join(" ")},uuid:function(){let t,e,n="";for(t=0;t<32;t++)e=16*Math.random()|0,8!=t&&12!=t&&16!=t&&20!=t||(n+="-"),n+=(12==t?4:16==t?3&e|8:e).toString(16);return n}},jt=(t,e)=>{const n=Object.assign({url:"/",field:"file",method:"POST",attributes:{},complete:function(){},error:function(){},success:function(){},progress:function(){}},e),s=new FormData;s.append(n.field,t,t.name),n.attributes&&Object.keys(n.attributes).forEach((t=>{s.append(t,n.attributes[t])}));const i=new XMLHttpRequest,o=e=>{if(!e.lengthComputable||!n.progress)return;let s=Math.max(0,Math.min(100,e.loaded/e.total*100));n.progress(i,t,Math.ceil(s))};i.upload.addEventListener("loadstart",o),i.upload.addEventListener("progress",o),i.addEventListener("load",(e=>{let s=null;try{s=JSON.parse(e.target.response)}catch(o){s={status:"error",message:"The file could not be uploaded"}}"error"===s.status?n.error(i,t,s):(n.success(i,t,s),n.progress(i,t,100))})),i.addEventListener("error",(e=>{const s=JSON.parse(e.target.response);n.error(i,t,s),n.progress(i,t,100)})),i.open(n.method,n.url,!0),n.headers&&Object.keys(n.headers).forEach((t=>{const e=n.headers[t];i.setRequestHeader(t,e)})),i.send(s)},Dt={install(t){Array.prototype.sortBy=function(e){const n=t.prototype.$helper.sort(),s=e.split(" "),i=s[0],o=s[1]||"asc";return this.sort(((t,e)=>{const s=String(t[i]).toLowerCase(),r=String(e[i]).toLowerCase();return"desc"===o?n(r,s):n(s,r)}))},t.prototype.$helper={clipboard:bt,clone:it.clone,color:yt,embed:xt,isComponent:St,isUploadEvent:Ct,debounce:$t,keyboard:Ot,object:it,pad:Lt.pad,ratio:Et,slug:Lt.slug,sort:Tt,string:Lt,upload:jt,uuid:Lt.uuid},t.prototype.$esc=Lt.escapeHTML}},Bt={install(t){t.$t=t.prototype.$t=window.panel.$t=(t,e,n=null)=>{if("string"!=typeof t)return;const s=window.panel.$translation.data[t]||n;return"string"!=typeof s?s:Mt(s,e)},t.directive("direction",{inserted(t,e,n){!0!==n.context.disabled?t.dir=n.context.$direction:t.dir=null}})}};p.extend(h),p.extend(((t,e,n)=>{n.interpret=(t,e="date")=>{const s={date:{"YYYY-MM-DD":!0,"YYYY-MM-D":!0,"YYYY-MM-":!0,"YYYY-MM":!0,"YYYY-M-DD":!0,"YYYY-M-D":!0,"YYYY-M-":!0,"YYYY-M":!0,"YYYY-":!0,YYYYMMDD:!0,"MMM DD YYYY":!1,"MMM D YYYY":!1,"MMM DD YY":!1,"MMM D YY":!1,"MMM DD":!1,"MMM D":!1,"DD MMMM YYYY":!1,"DD MMMM YY":!1,"DD MMMM":!1,"D MMMM YYYY":!1,"D MMMM YY":!1,"D MMMM":!1,"DD MMM YYYY":!1,"D MMM YYYY":!1,"DD MMM YY":!1,"D MMM YY":!1,"DD MMM":!1,"D MMM":!1,"DD MM YYYY":!1,"DD M YYYY":!1,"D MM YYYY":!1,"D M YYYY":!1,"DD MM YY":!1,"D MM YY":!1,"DD M YY":!1,"D M YY":!1,YYYY:!0,MMMM:!0,MMM:!0,"DD MM":!1,"DD M":!1,"D MM":!1,"D M":!1,DD:!1,D:!1},time:{"HH:mm:ss a":!1,"HH:mm:ss":!1,"HH:mm a":!1,"HH:mm":!1,"HH a":!1,HH:!1}};if("string"==typeof t&&""!==t)for(const i in s[e]){const o=n(t,i,s[e][i]);if(!0===o.isValid())return o}return null}})),p.extend(((t,e,n)=>{const s=t=>"date"===t?"YYYY-MM-DD":"time"===t?"HH:mm:ss":"YYYY-MM-DD HH:mm:ss";e.prototype.toISO=function(t="datetime"){return this.format(s(t))},n.iso=function(t,e="datetime"){const i=n(t,s(e));return i&&i.isValid()?i:null}})),p.extend(((t,e)=>{e.prototype.merge=function(t,e="date"){let n=this.clone();if(!t||!t.isValid())return this;if("string"==typeof e){const t={date:["year","month","date"],time:["hour","minute","second"]};if(!1===Object.prototype.hasOwnProperty.call(t,e))throw new Error("Invalid merge unit alias");e=t[e]}for(const s of e)n=n.set(s,t.get(s));return n}})),p.extend(((t,e,n)=>{n.pattern=t=>new class{constructor(t,e){this.dayjs=t,this.pattern=e;const n={year:["YY","YYYY"],month:["M","MM","MMM","MMMM"],day:["D","DD"],hour:["h","hh","H","HH"],minute:["m","mm"],second:["s","ss"],meridiem:["a"]};this.parts=this.pattern.split(/\W/).map(((t,e)=>{const s=this.pattern.indexOf(t);return{index:e,unit:Object.keys(n)[Object.values(n).findIndex((e=>e.includes(t)))],start:s,end:s+(t.length-1)}}))}at(t,e=t){const n=this.parts.filter((n=>n.start<=t&&n.end>=e-1));return n[0]?n[0]:this.parts.filter((e=>e.start<=t)).pop()}format(t){return t&&t.isValid()?t.format(this.pattern):null}}(n,t)})),p.extend(((t,e)=>{e.prototype.round=function(t="date",e=1){const n=["second","minute","hour","date","month","year"];if("day"===t&&(t="date"),!1===n.includes(t))throw new Error("Invalid rounding unit");if(["date","month","year"].includes(t)&&1!==e||"hour"===t&&24%e!=0||["second","minute"].includes(t)&&60%e!=0)throw"Invalid rounding size for "+t;let s=this.clone();const i=n.indexOf(t),o=n.slice(0,i),r=o.pop();if(o.forEach((t=>s=s.startOf(t))),r){const e={month:12,date:s.daysInMonth(),hour:24,minute:60,second:60}[r];Math.round(s.get(r)/e)*e===e&&(s=s.add(1,"date"===t?"day":t)),s=s.startOf(t)}return s=s.set(t,Math.round(s.get(t)/e)*e),s}})),p.extend(((t,e,n)=>{e.prototype.validate=function(t,e,s="day"){if(!this.isValid())return!1;if(!t)return!0;t=n.iso(t);const i={min:"isAfter",max:"isBefore"}[e];return this.isSame(t,s)||this[i](t,s)}}));var Pt={install(t){t.prototype.$library={autosize:f,dayjs:p}}},Nt={props:{blueprint:String,lock:[Boolean,Object],help:String,name:String,parent:String,timestamp:Number},methods:{load(){return this.$api.get(this.parent+"/sections/"+this.name)}}},qt={install(t){const e=a({},t.options.components),n={section:Nt};for(const[s,i]of Object.entries(window.panel.plugins.components))i.template||i.render||i.extends?("string"==typeof(null==i?void 0:i.extends)&&(e[i.extends]?i.extends=e[i.extends].extend({options:i,components:a(a({},e),i.components||{})}):i.extends=null),i.template&&(i.render=null),i.mixins&&(i.mixins=i.mixins.map((t=>"string"==typeof t?n[t]:t))),e[s]&&window.console.warn(`Plugin is replacing "${s}"`),t.component(s,i),e[s]=t.options.components[s]):ut.dispatch("notification/error",`Neither template or render method provided nor extending a component when loading plugin component "${s}". The component has not been registered.`);for(const s of window.panel.plugins.use)t.use(s)}};function Rt(t,e,n,s,i,o,r,a){var l,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=n,u._compiled=!0),s&&(u.functional=!0),o&&(u._scopeId="data-v-"+o),r?(l=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),i&&i.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(r)},u._ssrRegister=l):i&&(l=a?function(){i.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:i),l)if(u.functional){u._injectStyles=l;var c=u.render;u.render=function(t,e){return l.call(e),c(t,e)}}else{var d=u.beforeCreate;u.beforeCreate=d?[].concat(d,l):[l]}return{exports:t,options:u}}const Ft={props:{autofocus:{type:Boolean,default:!0},cancelButton:{type:[String,Boolean],default:!0},icon:{type:String,default:"check"},size:{type:String,default:"default"},submitButton:{type:[String,Boolean],default:!0},theme:String,visible:Boolean},data:()=>({notification:null}),computed:{buttons(){let t=[];return this.cancelButton&&t.push({icon:"cancel",text:this.cancelButtonLabel,class:"k-dialog-button-cancel",click:this.cancel}),this.submitButtonConfig&&t.push({icon:this.icon,text:this.submitButtonLabel,theme:this.theme,class:"k-dialog-button-submit",click:this.submit}),t},cancelButtonLabel(){return!1!==this.cancelButton&&(!0===this.cancelButton||0===this.cancelButton.length?this.$t("cancel"):this.cancelButton)},submitButtonConfig(){return void 0!==this.$attrs.button?this.$attrs.button:void 0===this.submitButton||this.submitButton},submitButtonLabel(){return!0===this.submitButton||0===this.submitButton.length?this.$t("confirm"):this.submitButton}},created(){this.$events.$on("keydown.esc",this.close,!1)},destroyed(){this.$events.$off("keydown.esc",this.close,!1)},mounted(){this.visible&&this.$nextTick(this.open)},methods:{onOverlayClose(){this.notification=null,this.$emit("close"),this.$events.$off("keydown.esc",this.close),this.$store.dispatch("dialog",!1)},open(){this.$store.state.dialog||this.$store.dispatch("dialog",!0),this.notification=null,this.$refs.overlay.open(),this.$emit("open"),this.$events.$on("keydown.esc",this.close)},close(){this.$refs.overlay&&this.$refs.overlay.close()},cancel(){this.$emit("cancel"),this.close()},focus(){var t;if(null==(t=this.$refs.dialog)?void 0:t.querySelector){const t=this.$refs.dialog.querySelector(".k-dialog-button-cancel");"function"==typeof(null==t?void 0:t.focus)&&t.focus()}},error(t){this.notification={message:t,type:"error"}},submit(){this.$emit("submit")},success(t){this.notification={message:t,type:"success"}}}},zt={};var Yt=Rt(Ft,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-overlay",{ref:"overlay",attrs:{autofocus:t.autofocus,centered:!0},on:{close:t.onOverlayClose,ready:function(e){return t.$emit("ready")}}},[n("div",{ref:"dialog",staticClass:"k-dialog",class:t.$vnode.data.staticClass,attrs:{"data-size":t.size},on:{mousedown:function(t){t.stopPropagation()}}},[t.notification?n("div",{staticClass:"k-dialog-notification",attrs:{"data-theme":t.notification.type}},[n("p",[t._v(t._s(t.notification.message))]),n("k-button",{attrs:{icon:"cancel"},on:{click:function(e){t.notification=null}}})],1):t._e(),n("div",{staticClass:"k-dialog-body scroll-y-auto"},[t._t("default")],2),t.$slots.footer||t.buttons.length?n("footer",{staticClass:"k-dialog-footer"},[t._t("footer",(function(){return[n("k-button-group",{attrs:{buttons:t.buttons}})]}))],2):t._e()])])}),[],!1,Ht,null,null,null);function Ht(t){for(let e in zt)this[e]=zt[e]}var Ut=function(){return Yt.exports}(),Kt={props:{autofocus:{type:Boolean,default:!0},cancelButton:{type:[String,Boolean],default:!0},icon:String,submitButton:{type:[String,Boolean],default:!0},size:String,theme:String,visible:Boolean},methods:{close(){this.$refs.dialog.close(),this.$emit("close")},error(t){this.$refs.dialog.error(t)},open(){this.$refs.dialog.open(),this.$emit("open")},success(t){this.$refs.dialog.close(),t.route&&this.$go(t.route),t.message&&this.$store.dispatch("notification/success",t.message),t.event&&("string"==typeof t.event&&(t.event=[t.event]),t.event.forEach((e=>{this.$events.$emit(e,t)}))),!1!==Object.prototype.hasOwnProperty.call(t,"emit")&&!1===t.emit||this.$emit("success")}}};const Jt={};var Gt=Rt({mixins:[Kt],props:{details:[Object,Array],message:String,size:{type:String,default:"medium"}},computed:{detailsList(){return Array.isArray(this.details)?this.details:Object.values(this.details||{})}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-error-dialog",attrs:{"cancel-button":!1,size:t.size,visible:!0},on:{cancel:function(e){return t.$emit("cancel")},close:function(e){return t.$emit("close")},submit:function(e){return t.$refs.dialog.close()}}},[n("k-text",[t._v(t._s(t.message))]),t.detailsList.length?n("dl",{staticClass:"k-error-details"},[t._l(t.detailsList,(function(e,s){return[n("dt",{key:"detail-label-"+s},[t._v(" "+t._s(e.label)+" ")]),n("dd",{key:"detail-message-"+s},["object"==typeof e.message?[n("ul",t._l(e.message,(function(e,s){return n("li",{key:s},[t._v(" "+t._s(e)+" ")])})),0)]:[t._v(" "+t._s(e.message)+" ")]],2)]}))],2):t._e()],1)}),[],!1,Vt,null,null,null);function Vt(t){for(let e in Jt)this[e]=Jt[e]}var Wt=function(){return Gt.exports}();const Xt={};var Zt=Rt({props:{code:Number,component:String,path:String,props:Object,referrer:String},methods:{close(){this.$refs.dialog.close()},onCancel(){"function"==typeof this.$store.state.dialog.cancel&&this.$store.state.dialog.cancel({dialog:this})},async onSubmit(t){let e=null;try{if("function"==typeof this.$store.state.dialog.submit)e=await this.$store.state.dialog.submit({dialog:this,value:t});else{if(!this.path)throw"The dialog needs a submit action or a dialog route path to be submitted";e=await this.$request(this.path,{body:t,method:"POST",type:"$dialog",headers:{"X-Fiber-Referrer":this.referrer}})}if(!1===e)return!1;this.close(),this.$store.dispatch("notification/success",":)"),e.event&&("string"==typeof e.event&&(e.event=[e.event]),e.event.forEach((t=>{this.$events.$emit(t,e)}))),e.dispatch&&Object.keys(e.dispatch).forEach((t=>{const n=e.dispatch[t];this.$store.dispatch(t,!0===Array.isArray(n)?[...n]:n)})),e.redirect?this.$go(e.redirect):this.$reload(e.reload||{})}catch(n){this.$refs.dialog.error(n)}}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)(t.component,t._b({ref:"dialog",tag:"component",attrs:{visible:!0},on:{cancel:t.onCancel,submit:t.onSubmit}},"component",t.props,!1))}),[],!1,Qt,null,null,null);function Qt(t){for(let e in Xt)this[e]=Xt[e]}var te=function(){return Zt.exports}(),ee={data:()=>({models:[],issue:null,selected:{},options:{endpoint:null,max:null,multiple:!0,parent:null,selected:[],search:!0},search:null,pagination:{limit:20,page:1,total:0}}),computed:{checkedIcon(){return!0===this.multiple?"check":"circle-filled"},items(){return this.models.map(this.item)},multiple(){return!0===this.options.multiple&&1!==this.options.max}},watch:{search(){this.updateSearch()}},created(){this.updateSearch=$t(this.updateSearch,200)},methods:{async fetch(){const t=a({page:this.pagination.page,search:this.search},this.fetchData||{});try{const e=await this.$api.get(this.options.endpoint,t);this.models=e.data,this.pagination=e.pagination,this.onFetched&&this.onFetched(e)}catch(e){this.models=[],this.issue=e.message}},async open(t,e){this.pagination.page=0,this.search=null;let n=!0;Array.isArray(t)?(this.models=t,n=!1):(this.models=[],e=t),this.options=a(a({},this.options),e),this.selected={},this.options.selected.forEach((t=>{this.$set(this.selected,t,{id:t})})),n&&await this.fetch(),this.$refs.dialog.open()},paginate(t){this.pagination.page=t.page,this.pagination.limit=t.limit,this.fetch()},submit(){this.$emit("submit",Object.values(this.selected)),this.$refs.dialog.close()},isSelected(t){return void 0!==this.selected[t.id]},item:t=>t,toggle(t){!1!==this.options.multiple&&1!==this.options.max||(this.selected={}),!0!==this.isSelected(t)?this.options.max&&this.options.max<=Object.keys(this.selected).length||this.$set(this.selected,t.id,t):this.$delete(this.selected,t.id)},toggleBtn(t){const e=this.isSelected(t);return{autofocus:!0,icon:e?this.checkedIcon:"circle-outline",tooltip:e?this.$t("remove"):this.$t("select"),theme:e?"positive":null}},updateSearch(){this.pagination.page=0,this.fetch()}}};const ne={};var se=Rt({mixins:[ee]},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-files-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.options.search?n("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),t.items.length?[n("k-items",{attrs:{link:!1,items:t.items,layout:"list",sortable:!1},on:{item:t.toggle},scopedSlots:t._u([{key:"options",fn:function(e){var s=e.item;return[n("k-button",t._b({on:{click:function(e){return t.toggle(s)}}},"k-button",t.toggleBtn(s),!1))]}}],null,!1,4112065674)}),n("k-pagination",t._b({staticClass:"k-dialog-pagination",attrs:{details:!0,dropdown:!1,align:"center"},on:{paginate:t.paginate}},"k-pagination",t.pagination,!1))]:n("k-empty",{attrs:{icon:"image"}},[t._v(" "+t._s(t.$t("dialog.files.empty"))+" ")])]],2)}),[],!1,ie,null,null,null);function ie(t){for(let e in ne)this[e]=ne[e]}var oe=function(){return se.exports}();const re={mixins:[Kt],props:{fields:{type:[Array,Object],default:()=>[]},novalidate:{type:Boolean,default:!0},size:{type:String,default:"medium"},submitButton:{type:[String,Boolean],default:()=>window.panel.$t("save")},text:{type:String},theme:{type:String,default:"positive"},value:{type:Object,default:()=>({})}},data(){return{model:this.value}},computed:{hasFields(){return Object.keys(this.fields).length>0}},watch:{value(t,e){t!==e&&(this.model=t)}}},ae={};var le=Rt(re,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",t._b({ref:"dialog",on:{cancel:function(e){return t.$emit("cancel")},close:function(e){return t.$emit("close")},ready:function(e){return t.$emit("ready")},submit:function(e){return t.$refs.form.submit()}}},"k-dialog",t.$props,!1),[t.text?[n("k-text",{domProps:{innerHTML:t._s(t.text)}})]:t._e(),t.hasFields?n("k-form",{ref:"form",attrs:{value:t.model,fields:t.fields,novalidate:t.novalidate},on:{input:function(e){return t.$emit("input",e)},submit:function(e){return t.$emit("submit",e)}}}):n("k-box",{attrs:{theme:"negative"}},[t._v(" This form dialog has no fields ")])],2)}),[],!1,ue,null,null,null);function ue(t){for(let e in ae)this[e]=ae[e]}var ce=function(){return le.exports}();const de={};var pe=Rt({extends:ce,watch:{"model.name"(t){this.fields.code.disabled||this.onNameChanges(t)},"model.code"(t){this.fields.code.disabled||(this.model.code=this.$helper.slug(t,[this.$system.ascii]),this.onCodeChanges(this.model.code))}},methods:{onCodeChanges(t){if(!t)return this.model.locale=null;if(t.length>=2)if(-1!==t.indexOf("-")){let e=t.split("-"),n=[e[0],e[1].toUpperCase()];this.model.locale=n.join("_")}else{let e=this.$system.locales||[];(null==e?void 0:e[t])?this.model.locale=e[t]:this.model.locale=null}},onNameChanges(t){this.model.code=this.$helper.slug(t,[this.model.rules,this.$system.ascii]).substr(0,2)}}},undefined,undefined,!1,he,null,null,null);function he(t){for(let e in de)this[e]=de[e]}var fe=function(){return pe.exports}();const me={};var ge=Rt({mixins:[ee],data(){const t=ee.data();return l(a({},t),{model:{title:null,parent:null},options:l(a({},t.options),{parent:null})})},computed:{fetchData(){return{parent:this.options.parent}}},methods:{back(){this.options.parent=this.model.parent,this.pagination.page=1,this.fetch()},go(t){this.options.parent=t.id,this.pagination.page=1,this.fetch()},onFetched(t){this.model=t.model}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-pages-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.model?n("header",{staticClass:"k-pages-dialog-navbar"},[n("k-button",{attrs:{disabled:!t.model.id,tooltip:t.$t("back"),icon:"angle-left"},on:{click:t.back}}),n("k-headline",[t._v(t._s(t.model.title))])],1):t._e(),t.options.search?n("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),t.items.length?[n("k-items",{attrs:{items:t.items,link:!1,layout:"list",sortable:!1},on:{item:t.toggle},scopedSlots:t._u([{key:"options",fn:function(e){var s=e.item;return[n("k-button",t._b({on:{click:function(e){return t.toggle(s)}}},"k-button",t.toggleBtn(s),!1)),s?n("k-button",{attrs:{disabled:!s.hasChildren,tooltip:t.$t("open"),icon:"angle-right"},on:{click:function(e){return e.stopPropagation(),t.go(s)}}}):t._e()]}}],null,!1,3385025206)}),n("k-pagination",t._b({staticClass:"k-dialog-pagination",attrs:{details:!0,dropdown:!1,align:"center"},on:{paginate:t.paginate}},"k-pagination",t.pagination,!1))]:n("k-empty",{attrs:{icon:"page"}},[t._v(" "+t._s(t.$t("dialog.pages.empty"))+" ")])]],2)}),[],!1,ke,null,null,null);function ke(t){for(let e in me)this[e]=me[e]}var ve=function(){return ge.exports}();const be={mixins:[Kt],props:{icon:{type:String,default:"trash"},submitButton:{type:[String,Boolean],default:()=>window.panel.$t("delete")},text:String,theme:{type:String,default:"negative"}}},ye={};var $e=Rt(be,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-text-dialog",t._g(t._b({ref:"dialog"},"k-text-dialog",t.$props,!1),t.$listeners),[t._t("default")],2)}),[],!1,_e,null,null,null);function _e(t){for(let e in ye)this[e]=ye[e]}var we=function(){return $e.exports}();const xe={};var Se=Rt({mixins:[Kt],props:{text:String}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",t._g(t._b({ref:"dialog"},"k-dialog",t.$props,!1),t.$listeners),[t._t("default",(function(){return[t.text?n("k-text",{domProps:{innerHTML:t._s(t.text)}}):n("k-box",{attrs:{theme:"negative"}},[t._v(" This dialog does not define any text ")])]}))],2)}),[],!1,Ce,null,null,null);function Ce(t){for(let e in xe)this[e]=xe[e]}var Oe=function(){return Se.exports}();const Ee={};var Te=Rt({mixins:[ee],methods:{item:t=>l(a({},t),{key:t.email,info:t.info!==t.text?t.info:null})}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-users-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[n("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.options.search?n("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),t.items.length?[n("k-items",{attrs:{items:t.items,link:!1,layout:"list",sortable:!1},on:{item:t.toggle},scopedSlots:t._u([{key:"options",fn:function(e){var s=e.item;return[n("k-button",t._b({on:{click:function(e){return t.toggle(s)}}},"k-button",t.toggleBtn(s),!1))]}}],null,!1,409892637)}),n("k-pagination",t._b({staticClass:"k-dialog-pagination",attrs:{details:!0,dropdown:!1,align:"center"},on:{paginate:t.paginate}},"k-pagination",t.pagination,!1))]:n("k-empty",{attrs:{icon:"users"}},[t._v(" "+t._s(t.$t("dialog.users.empty"))+" ")])]],2)}),[],!1,Ae,null,null,null);function Ae(t){for(let e in Ee)this[e]=Ee[e]}var Me=function(){return Te.exports}();const Ie={};var Le=Rt({inheritAttrs:!1,props:{id:String,icon:String,tab:String,tabs:Object,title:String},data:()=>({click:!1}),computed:{breadcrumb(){return this.$store.state.drawers.open},hasTabs(){return this.tabs&&Object.keys(this.tabs).length>1},index(){return this.breadcrumb.findIndex((t=>t.id===this._uid))},nested(){return this.index>0}},watch:{index(){-1===this.index&&this.close()}},destroyed(){this.$store.dispatch("drawers/close",this._uid)},methods:{close(){this.$refs.overlay.close()},goTo(t){if(t===this._uid)return!0;this.$store.dispatch("drawers/goto",t)},mouseup(){!0===this.click&&this.close(),this.click=!1},mousedown(t=!1){this.click=t,!0===this.click&&this.$store.dispatch("drawers/close")},onClose(){this.$store.dispatch("drawers/close",this._uid),this.$emit("close")},onOpen(){this.$store.dispatch("drawers/open",{id:this._uid,icon:this.icon,title:this.title}),this.$emit("open")},open(){this.$refs.overlay.open()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-overlay",{ref:"overlay",attrs:{dimmed:!1},on:{close:t.onClose,open:t.onOpen}},[n("div",{staticClass:"k-drawer",attrs:{"data-id":t.id,"data-nested":t.nested},on:{mousedown:function(e){return e.stopPropagation(),t.mousedown(!0)},mouseup:t.mouseup}},[n("div",{staticClass:"k-drawer-box",on:{mousedown:function(e){return e.stopPropagation(),t.mousedown(!1)}}},[n("header",{staticClass:"k-drawer-header"},[1===t.breadcrumb.length?n("h2",{staticClass:"k-drawer-title"},[n("k-icon",{attrs:{type:t.icon}}),t._v(" "+t._s(t.title)+" ")],1):n("ul",{staticClass:"k-drawer-breadcrumb"},t._l(t.breadcrumb,(function(e){return n("li",{key:e.id},[n("k-button",{attrs:{icon:e.icon,text:e.title},on:{click:function(n){return t.goTo(e.id)}}})],1)})),0),t.hasTabs?n("nav",{staticClass:"k-drawer-tabs"},t._l(t.tabs,(function(e){return n("k-button",{key:e.name,staticClass:"k-drawer-tab",attrs:{current:t.tab==e.name,text:e.label},on:{click:function(n){return n.stopPropagation(),t.$emit("tab",e.name)}}})})),1):t._e(),n("nav",{staticClass:"k-drawer-options"},[t._t("options"),n("k-button",{staticClass:"k-drawer-option",attrs:{icon:"check"},on:{click:t.close}})],2)]),n("div",{staticClass:"k-drawer-body scroll-y-auto"},[t._t("default")],2)])])])}),[],!1,je,null,null,null);function je(t){for(let e in Ie)this[e]=Ie[e]}var De=function(){return Le.exports}(),Be=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-drawer",{ref:"drawer",staticClass:"k-form-drawer",attrs:{id:t.id,icon:t.icon,tabs:t.tabs,tab:t.tab,title:t.title},on:{close:function(e){return t.$emit("close")},open:function(e){return t.$emit("open")},tab:function(e){t.tab=e}},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options")]},proxy:!0},{key:"default",fn:function(){return[0===Object.keys(t.fields).length?n("k-box",{attrs:{theme:"info"}},[t._v(" "+t._s(t.empty)+" ")]):n("k-form",{ref:"form",attrs:{autofocus:!0,fields:t.fields,value:t.$helper.clone(t.value)},on:{input:function(e){return t.$emit("input",e)}}})]},proxy:!0}],null,!0)})};const Pe={};var Ne=Rt({inheritAttrs:!1,props:{empty:{type:String,default:()=>"Missing field setup"},icon:String,id:String,tabs:Object,title:String,type:String,value:Object},data:()=>({tab:null}),computed:{fields(){const t=this.tab||null;return(this.tabs[t]||this.firstTab).fields||{}},firstTab(){return Object.values(this.tabs)[0]}},methods:{close(){this.$refs.drawer.close()},focus(t){var e;"function"==typeof(null==(e=this.$refs.form)?void 0:e.focus)&&this.$refs.form.focus(t)},open(t,e=!0){this.$refs.drawer.open(),this.tab=t||this.firstTab.name,!1!==e&&setTimeout((()=>{let t=Object.values(this.fields).filter((t=>!0===t.autofocus))[0]||null;this.focus(t)}),1)}}},Be,[],!1,qe,null,null,null);function qe(t){for(let e in Pe)this[e]=Pe[e]}var Re=function(){return Ne.exports}();const Fe={props:{html:{type:Boolean,default:!1},limit:{type:Number,default:10},skip:{type:Array,default:()=>[]},options:Array,query:String},data:()=>({matches:[],selected:{text:null}}),methods:{close(){this.$refs.dropdown.close()},onSelect(t){this.$emit("select",t),this.$refs.dropdown.close()},search(t){if(t.length<1)return;const e=new RegExp(RegExp.escape(t),"ig");this.matches=this.options.filter((t=>!!t.text&&(-1===this.skip.indexOf(t.value)&&null!==t.text.match(e)))).slice(0,this.limit),this.$emit("search",t,this.matches),this.$refs.dropdown.open()}}},ze={};var Ye=Rt(Fe,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dropdown",{staticClass:"k-autocomplete"},[t._t("default"),n("k-dropdown-content",t._g({ref:"dropdown",attrs:{autofocus:!0}},t.$listeners),t._l(t.matches,(function(e,s){return n("k-dropdown-item",t._b({key:s,on:{mousedown:function(n){return t.onSelect(e)},keydown:[function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"tab",9,n.key,"Tab")?null:(n.preventDefault(),t.onSelect(e))},function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"enter",13,n.key,"Enter")?null:(n.preventDefault(),t.onSelect(e))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])||"button"in e&&0!==e.button?null:(e.preventDefault(),t.close.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)?null:(e.preventDefault(),t.close.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.close.apply(null,arguments))}]}},"k-dropdown-item",e,!1),[n("span",{domProps:{innerHTML:t._s(t.html?e.text:t.$esc(e.text))}})])})),1),t._v(" "+t._s(t.query)+" ")],2)}),[],!1,He,null,null,null);function He(t){for(let e in ze)this[e]=ze[e]}var Ue=function(){return Ye.exports}();const Ke={props:{disabled:Boolean,max:String,min:String,value:String},data(){return this.data(this.value)},computed:{numberOfDays(){return this.toDate().daysInMonth()},firstWeekday(){const t=this.toDate().day();return t>0?t:7},weekdays(){return["mon","tue","wed","thu","fri","sat","sun"].map((t=>this.$t("days."+t)))},weeks(){const t=this.firstWeekday-1;return Math.ceil((this.numberOfDays+t)/7)},monthnames(){return["january","february","march","april","may","june","july","august","september","october","november","december"].map((t=>this.$t("months."+t)))},months(){var t=[];return this.monthnames.forEach(((e,n)=>{const s=this.toDate(1,n);t.push({value:n,text:e,disabled:s.isBefore(this.current.min,"month")||s.isAfter(this.current.max,"month")})})),t},years(){var t,e,n,s;const i=null!=(e=null==(t=this.current.min)?void 0:t.get("year"))?e:this.current.year-20,o=null!=(s=null==(n=this.current.max)?void 0:n.get("year"))?s:this.current.year+20;return this.toOptions(i,o)}},watch:{value(t){const e=this.data(t);this.dt=e.dt,this.current=e.current}},methods:{data(t){const e=this.$library.dayjs.iso(t),n=this.$library.dayjs();return{dt:e,current:{month:(null!=e?e:n).month(),year:(null!=e?e:n).year(),min:this.$library.dayjs.iso(this.min),max:this.$library.dayjs.iso(this.max)}}},days(t){let e=[];const n=7*(t-1)+1,s=n+7;for(let i=n;ithis.numberOfDays;e.push(n?"":t)}return e},isDisabled(t){const e=this.toDate(t);return this.disabled||e.isBefore(this.current.min,"day")||e.isAfter(this.current.max,"day")},isSelected(t){return this.toDate(t).isSame(this.dt,"day")},isToday(t){const e=this.$library.dayjs();return this.toDate(t).isSame(e,"day")},onInput(){var t;this.$emit("input",(null==(t=this.dt)?void 0:t.toISO("date"))||null)},onNext(){const t=this.toDate().add(1,"month");this.show(t)},onPrev(){const t=this.toDate().subtract(1,"month");this.show(t)},select(t){const e="today"===t?this.$library.dayjs().merge(this.toDate(),"time"):this.toDate(t);this.dt=e,this.show(e),this.onInput()},show(t){this.current.year=t.year(),this.current.month=t.month()},toDate(t=1,e=this.current.month){return this.$library.dayjs(`${this.current.year}-${e+1}-${t}`)},toOptions(t,e){for(var n=[],s=t;s<=e;s++)n.push({value:s,text:this.$helper.pad(s)});return n}}},Je={};var Ge=Rt(Ke,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-calendar-input"},[n("nav",[n("k-button",{attrs:{icon:"angle-left"},on:{click:t.onPrev}}),n("span",{staticClass:"k-calendar-selects"},[n("k-select-input",{attrs:{options:t.months,disabled:t.disabled,required:!0},model:{value:t.current.month,callback:function(e){t.$set(t.current,"month",t._n(e))},expression:"current.month"}}),n("k-select-input",{attrs:{options:t.years,disabled:t.disabled,required:!0},model:{value:t.current.year,callback:function(e){t.$set(t.current,"year",t._n(e))},expression:"current.year"}})],1),n("k-button",{attrs:{icon:"angle-right"},on:{click:t.onNext}})],1),n("table",{staticClass:"k-calendar-table"},[n("thead",[n("tr",t._l(t.weekdays,(function(e){return n("th",{key:"weekday_"+e},[t._v(" "+t._s(e)+" ")])})),0)]),n("tbody",t._l(t.weeks,(function(e){return n("tr",{key:"week_"+e},t._l(t.days(e),(function(e,s){return n("td",{key:"day_"+s,staticClass:"k-calendar-day",attrs:{"aria-current":!!t.isToday(e)&&"date","aria-selected":!!t.isSelected(e)&&"date"}},[e?n("k-button",{attrs:{disabled:t.isDisabled(e),text:e},on:{click:function(n){return t.select(e)}}}):t._e()],1)})),0)})),0),n("tfoot",[n("tr",[n("td",{staticClass:"k-calendar-today",attrs:{colspan:"7"}},[n("k-button",{attrs:{text:t.$t("today")},on:{click:function(e){return t.select("today")}}})],1)])])])])}),[],!1,Ve,null,null,null);function Ve(t){for(let e in Je)this[e]=Je[e]}var We=function(){return Ge.exports}();const Xe={props:{count:Number,min:Number,max:Number,required:{type:Boolean,default:!1}},computed:{valid(){return!1===this.required&&0===this.count||(!0!==this.required||0!==this.count)&&(!(this.min&&this.countthis.max))}}},Ze={};var Qe=Rt(Xe,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-counter",attrs:{"data-invalid":!t.valid}},[n("span",[t._v(t._s(t.count))]),t.min&&t.max?n("span",{staticClass:"k-counter-rules"},[t._v("("+t._s(t.min)+"–"+t._s(t.max)+")")]):t.min?n("span",{staticClass:"k-counter-rules"},[t._v("≥ "+t._s(t.min))]):t.max?n("span",{staticClass:"k-counter-rules"},[t._v("≤ "+t._s(t.max))]):t._e()])}),[],!1,tn,null,null,null);function tn(t){for(let e in Ze)this[e]=Ze[e]}var en=function(){return Qe.exports}();const nn={props:{disabled:Boolean,config:Object,fields:{type:[Array,Object],default:()=>({})},novalidate:{type:Boolean,default:!1},value:{type:Object,default:()=>({})}},data(){return{errors:{},listeners:l(a({},this.$listeners),{submit:this.onSubmit})}},methods:{focus(t){var e,n;null==(n=null==(e=this.$refs.fields)?void 0:e.focus)||n.call(e,t)},onSubmit(){this.$emit("submit",this.value)},submit(){this.$refs.submitter.click()}}},sn={};var on=Rt(nn,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("form",{ref:"form",staticClass:"k-form",attrs:{method:"POST",autocomplete:"off",novalidate:""},on:{submit:function(e){return e.preventDefault(),t.onSubmit.apply(null,arguments)}}},[t._t("header"),t._t("default",(function(){return[n("k-fieldset",t._g({ref:"fields",attrs:{disabled:t.disabled,fields:t.fields,novalidate:t.novalidate},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}},t.listeners))]})),t._t("footer"),n("input",{ref:"submitter",staticClass:"k-form-submitter",attrs:{type:"submit"}})],2)}),[],!1,rn,null,null,null);function rn(t){for(let e in sn)this[e]=sn[e]}var an=function(){return on.exports}();const ln={props:{lock:[Boolean,Object]},data:()=>({isRefreshing:null,isLocking:null}),computed:{hasChanges(){return this.$store.getters["content/hasChanges"]()},isDisabled(){return!1===this.$store.state.content.status.enabled},isLocked(){return"lock"===this.lockState},isUnlocked(){return"unlock"===this.lockState},mode(){return null!==this.lockState?this.lockState:!0===this.hasChanges?"changes":null},lockState(){return this.supportsLocking&&this.lock?this.lock.state:null},supportsLocking(){return!1!==this.lock},theme(){return"lock"===this.mode?"negative":"unlock"===this.mode?"info":"notice"}},watch:{hasChanges:{handler(t,e){!0===this.supportsLocking&&!1===this.isLocked&&!1===this.isUnlocked&&(!0===t?(this.onLock(),this.isLocking=setInterval(this.onLock,3e4)):e&&(clearInterval(this.isLocking),this.onLock(!1)))},immediate:!0},isLocked(t){!1===t&&this.$events.$emit("model.reload")}},created(){this.supportsLocking&&(this.isRefreshing=setInterval(this.check,1e4)),this.$events.$on("keydown.cmd.s",this.onSave)},destroyed(){clearInterval(this.isRefreshing),clearInterval(this.isLocking),this.$events.$off("keydown.cmd.s",this.onSave)},methods:{check(){this.$reload({navigate:!1,only:"$view.props.lock",silent:!0})},async onLock(t=!0){const e=[this.$view.path+"/lock",null,null,!0];if(!0===t)try{await this.$api.patch(...e)}catch(n){clearInterval(this.isLocking),this.$store.dispatch("content/revert")}else clearInterval(this.isLocking),await this.$api.delete(...e)},onDownload(){let t="";const e=this.$store.getters["content/changes"]();Object.keys(e).forEach((n=>{t+=n+": \n\n"+e[n],t+="\n\n----\n\n"}));let n=document.createElement("a");n.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(t)),n.setAttribute("download",this.$view.path+".txt"),n.style.display="none",document.body.appendChild(n),n.click(),document.body.removeChild(n)},async onResolve(){await this.onUnlock(!1),this.$store.dispatch("content/revert")},onRevert(){this.$refs.revert.open()},async onSave(t){if(!t)return!1;t.preventDefault&&t.preventDefault();try{await this.$store.dispatch("content/save"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")}catch(e){if(403===e.code)return;e.details&&Object.keys(e.details).length>0?this.$store.dispatch("notification/error",{message:this.$t("error.form.incomplete"),details:e.details}):this.$store.dispatch("notification/error",{message:this.$t("error.form.notSaved"),details:[{label:"Exception: "+e.exception,message:e.message}]})}},async onUnlock(t=!0){const e=[this.$view.path+"/unlock",null,null,!0];!0===t?await this.$api.patch(...e):await this.$api.delete(...e),this.$reload({silent:!0})},revert(){this.$store.dispatch("content/revert"),this.$refs.revert.close()}}},un={};var cn=Rt(ln,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-form-buttons",attrs:{"data-theme":t.theme}},["unlock"===t.mode?n("k-view",[n("p",{staticClass:"k-form-lock-info"},[t._v(" "+t._s(t.$t("lock.isUnlocked"))+" ")]),n("span",{staticClass:"k-form-lock-buttons"},[n("k-button",{staticClass:"k-form-button",attrs:{text:t.$t("download"),icon:"download"},on:{click:t.onDownload}}),n("k-button",{staticClass:"k-form-button",attrs:{text:t.$t("confirm"),icon:"check"},on:{click:t.onResolve}})],1)]):"lock"===t.mode?n("k-view",[n("p",{staticClass:"k-form-lock-info"},[n("k-icon",{attrs:{type:"lock"}}),n("span",{domProps:{innerHTML:t._s(t.$t("lock.isLocked",{email:t.$esc(t.lock.data.email)}))}})],1),t.lock.data.unlockable?n("k-button",{staticClass:"k-form-button",attrs:{text:t.$t("lock.unlock"),icon:"unlock"},on:{click:function(e){return t.onUnlock()}}}):n("k-icon",{staticClass:"k-form-lock-loader",attrs:{type:"loader"}})],1):"changes"===t.mode?n("k-view",[n("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,text:t.$t("revert"),icon:"undo"},on:{click:t.onRevert}}),n("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,text:t.$t("save"),icon:"check"},on:{click:t.onSave}})],1):t._e(),n("k-dialog",{ref:"revert",attrs:{"submit-button":t.$t("revert"),icon:"undo",theme:"negative"},on:{submit:t.revert}},[n("k-text",{domProps:{innerHTML:t._s(t.$t("revert.confirm"))}})],1)],1)}),[],!1,dn,null,null,null);function dn(t){for(let e in un)this[e]=un[e]}var pn=function(){return cn.exports}();const hn={};var fn=Rt({data:()=>({isOpen:!1,options:[]}),computed:{hasChanges(){return this.ids.length>0},ids(){return Object.keys(this.store).filter((t=>{var e;return Object.keys((null==(e=this.store[t])?void 0:e.changes)||{}).length>0}))},store(){return this.$store.state.content.models}},methods:{async toggle(){if(!1===this.$refs.list.isOpen)try{await this.$dropdown("changes",{method:"POST",body:{ids:this.ids}})((t=>{this.options=t}))}catch(t){return this.$store.dispatch("notification/success",this.$t("lock.unsaved.empty")),this.$store.dispatch("content/clear"),!1}this.$refs.list&&this.$refs.list.toggle()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.hasChanges?n("k-dropdown",{staticClass:"k-form-indicator"},[n("k-button",{staticClass:"k-form-indicator-toggle k-topbar-button",attrs:{icon:"edit"},on:{click:t.toggle}}),n("k-dropdown-content",{ref:"list",attrs:{align:"right",theme:"light"}},[n("p",{staticClass:"k-form-indicator-info"},[t._v(t._s(t.$t("lock.unsaved"))+":")]),n("hr"),t._l(t.options,(function(e){return n("k-dropdown-item",t._b({key:e.id},"k-dropdown-item",e,!1),[t._v(" "+t._s(e.text)+" ")])}))],2)],1):t._e()}),[],!1,mn,null,null,null);function mn(t){for(let e in hn)this[e]=hn[e]}var gn=function(){return fn.exports}(),kn={props:{after:String}},vn={props:{autofocus:Boolean}},bn={props:{before:String}},yn={props:{disabled:Boolean}},$n={props:{help:String}},_n={props:{id:{type:[Number,String],default(){return this._uid}}}},wn={props:{invalid:Boolean}},xn={props:{label:String}},Sn={props:{name:[Number,String]}},Cn={props:{required:Boolean}};const On={mixins:[yn,$n,xn,Sn,Cn],props:{counter:[Boolean,Object],endpoints:Object,input:[String,Number],translate:Boolean,type:String}},En={};var Tn=Rt({mixins:[On],inheritAttrs:!1,computed:{labelText(){return this.label||" "}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{class:"k-field k-field-name-"+t.name,attrs:{"data-disabled":t.disabled,"data-translate":t.translate},on:{focusin:function(e){return t.$emit("focus",e)},focusout:function(e){return t.$emit("blur",e)}}},[t._t("header",(function(){return[n("header",{staticClass:"k-field-header"},[t._t("label",(function(){return[n("label",{staticClass:"k-field-label",attrs:{for:t.input}},[t._v(" "+t._s(t.labelText)+" "),t.required?n("abbr",{attrs:{title:t.$t("field.required")}},[t._v("*")]):t._e()])]})),t._t("options"),t._t("counter",(function(){return[t.counter?n("k-counter",t._b({staticClass:"k-field-counter",attrs:{required:t.required}},"k-counter",t.counter,!1)):t._e()]}))],2)]})),t._t("default"),t._t("footer",(function(){return[t.help||t.$slots.help?n("footer",{staticClass:"k-field-footer"},[t._t("help",(function(){return[t.help?n("k-text",{staticClass:"k-field-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()]}))],2):t._e()]}))],2)}),[],!1,An,null,null,null);function An(t){for(let e in En)this[e]=En[e]}var Mn=function(){return Tn.exports}();const In={props:{config:Object,disabled:Boolean,fields:{type:[Array,Object],default:()=>[]},novalidate:{type:Boolean,default:!1},value:{type:Object,default:()=>({})}},data:()=>({errors:{}}),methods:{focus(t){if(t)return void(this.hasField(t)&&"function"==typeof this.$refs[t][0].focus&&this.$refs[t][0].focus());const e=Object.keys(this.$refs)[0];this.focus(e)},hasFieldType(t){return this.$helper.isComponent(`k-${t}-field`)},hasField(t){var e;return null==(e=this.$refs[t])?void 0:e[0]},meetsCondition(t){if(!t.when)return!0;let e=!0;return Object.keys(t.when).forEach((n=>{this.value[n.toLowerCase()]!==t.when[n]&&(e=!1)})),e},onInvalid(t,e,n,s){this.errors[s]=e,this.$emit("invalid",this.errors)},hasErrors(){return Object.keys(this.errors).length}}},Ln={};var jn=Rt(In,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("fieldset",{staticClass:"k-fieldset"},[n("k-grid",[t._l(t.fields,(function(e,s){return["hidden"!==e.type&&t.meetsCondition(e)?n("k-column",{key:e.signature,attrs:{width:e.width}},[n("k-error-boundary",[t.hasFieldType(e.type)?n("k-"+e.type+"-field",t._b({ref:s,refInFor:!0,tag:"component",attrs:{"form-data":t.value,name:s,novalidate:t.novalidate,disabled:t.disabled||e.disabled},on:{input:function(n){return t.$emit("input",t.value,e,s)},focus:function(n){return t.$emit("focus",n,e,s)},invalid:function(n,i){return t.onInvalid(n,i,e,s)},submit:function(n){return t.$emit("submit",n,e,s)}},model:{value:t.value[s],callback:function(e){t.$set(t.value,s,e)},expression:"value[fieldName]"}},"component",e,!1)):n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[t._v(" The field type "),n("strong",[t._v('"'+t._s(s)+'"')]),t._v(" does not exist ")])],1)],1)],1):t._e()]}))],2)],1)}),[],!1,Dn,null,null,null);function Dn(t){for(let e in Ln)this[e]=Ln[e]}var Bn=function(){return jn.exports}();const Pn={mixins:[kn,bn,yn,wn],props:{autofocus:Boolean,type:String,icon:[String,Boolean],theme:String,novalidate:{type:Boolean,default:!1},value:{type:[String,Boolean,Number,Object,Array],default:null}}},Nn={};var qn=Rt({mixins:[Pn],inheritAttrs:!1,data(){return{isInvalid:this.invalid,listeners:l(a({},this.$listeners),{invalid:(t,e)=>{this.isInvalid=t,this.$emit("invalid",t,e)}})}},computed:{inputProps(){return a(a({},this.$props),this.$attrs)}},methods:{blur(t){(null==t?void 0:t.relatedTarget)&&!1===this.$el.contains(t.relatedTarget)&&this.trigger(null,"blur")},focus(t){this.trigger(t,"focus")},select(t){this.trigger(t,"select")},trigger(t,e){var n,s,i;if("INPUT"===(null==(n=null==t?void 0:t.target)?void 0:n.tagName)&&"function"==typeof(null==(s=null==t?void 0:t.target)?void 0:s[e]))return void t.target[e]();if("function"==typeof(null==(i=this.$refs.input)?void 0:i[e]))return void this.$refs.input[e]();const o=this.$el.querySelector("input, select, textarea");"function"==typeof(null==o?void 0:o[e])&&o[e]()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-input",attrs:{"data-disabled":t.disabled,"data-invalid":!t.novalidate&&t.isInvalid,"data-theme":t.theme,"data-type":t.type}},[t.$slots.before||t.before?n("span",{staticClass:"k-input-before",on:{click:t.focus}},[t._t("before",(function(){return[t._v(t._s(t.before))]}))],2):t._e(),n("span",{staticClass:"k-input-element",on:{click:function(e){return e.stopPropagation(),t.focus.apply(null,arguments)}}},[t._t("default",(function(){return[n("k-"+t.type+"-input",t._g(t._b({ref:"input",tag:"component",attrs:{value:t.value}},"component",t.inputProps,!1),t.listeners))]}))],2),t.$slots.after||t.after?n("span",{staticClass:"k-input-after",on:{click:t.focus}},[t._t("after",(function(){return[t._v(t._s(t.after))]}))],2):t._e(),t.$slots.icon||t.icon?n("span",{staticClass:"k-input-icon",on:{click:t.focus}},[t._t("icon",(function(){return[n("k-icon",{attrs:{type:t.icon}})]}))],2):t._e()])}),[],!1,Rn,null,null,null);function Rn(t){for(let e in Nn)this[e]=Nn[e]}var Fn=function(){return qn.exports}();const zn={};var Yn=Rt({props:{methods:Array},data:()=>({currentForm:null,isLoading:!1,issue:"",user:{email:"",password:"",remember:!1}}),computed:{canToggle(){return null!==this.codeMode&&!0===this.methods.includes("password")&&(!0===this.methods.includes("password-reset")||!0===this.methods.includes("code"))},codeMode(){return!0===this.methods.includes("password-reset")?"password-reset":!0===this.methods.includes("code")?"code":null},fields(){let t={email:{autofocus:!0,label:this.$t("email"),type:"email",required:!0,link:!1}};return"email-password"===this.form&&(t.password={label:this.$t("password"),type:"password",minLength:8,required:!0,autocomplete:"current-password",counter:!1}),t},form(){return this.currentForm?this.currentForm:"password"===this.methods[0]?"email-password":"email"},isResetForm(){return"password-reset"===this.codeMode&&"email"===this.form},toggleText(){return this.$t("login.toggleText."+this.codeMode+"."+this.formOpposite(this.form))}},methods:{formOpposite:t=>"email-password"===t?"email":"email-password",async login(){this.issue=null,this.isLoading=!0;let t=Object.assign({},this.user);"email"===this.currentForm&&(t.password=null),!0===this.isResetForm&&(t.remember=!1);try{await this.$api.auth.login(t),this.$reload({globals:["$system","$translation"]})}catch(e){this.issue=e.message}finally{this.isLoading=!1}},toggleForm(){this.currentForm=this.formOpposite(this.form),this.$refs.fieldset.focus("email")}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("form",{staticClass:"k-login-form",on:{submit:function(e){return e.preventDefault(),t.login.apply(null,arguments)}}},[n("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("login"))+" ")]),t.issue?n("k-login-alert",{on:{click:function(e){t.issue=null}}},[t._v(" "+t._s(t.issue)+" ")]):t._e(),n("div",{staticClass:"k-login-fields"},[!0===t.canToggle?n("button",{staticClass:"k-login-toggler",attrs:{type:"button"},on:{click:t.toggleForm}},[t._v(" "+t._s(t.toggleText)+" ")]):t._e(),n("k-fieldset",{ref:"fieldset",attrs:{novalidate:!0,fields:t.fields},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1),n("div",{staticClass:"k-login-buttons"},[!1===t.isResetForm?n("span",{staticClass:"k-login-checkbox"},[n("k-checkbox-input",{attrs:{value:t.user.remember,label:t.$t("login.remember")},on:{input:function(e){t.user.remember=e}}})],1):t._e(),n("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v(" "+t._s(t.$t("login"+(t.isResetForm?".reset":"")))+" "),t.isLoading?[t._v(" … ")]:t._e()],2)],1)],1)}),[],!1,Hn,null,null,null);function Hn(t){for(let e in zn)this[e]=zn[e]}var Un=function(){return Yn.exports}();const Kn={};var Jn=Rt({props:{methods:Array,pending:Object},data:()=>({code:"",isLoadingBack:!1,isLoadingLogin:!1,issue:""}),computed:{mode(){return!0===this.methods.includes("password-reset")?"password-reset":"login"}},methods:{async back(){this.isLoadingBack=!0,this.$go("/logout")},async login(){this.issue=null,this.isLoadingLogin=!0;try{await this.$api.auth.verifyCode(this.code),this.$store.dispatch("notification/success",this.$t("welcome")),"password-reset"===this.mode?this.$go("reset-password"):this.$reload()}catch(t){this.issue=t.message}finally{this.isLoadingLogin=!1}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("form",{staticClass:"k-login-form k-login-code-form",on:{submit:function(e){return e.preventDefault(),t.login.apply(null,arguments)}}},[n("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("login"))+" ")]),t.issue?n("k-login-alert",{on:{click:function(e){t.issue=null}}},[t._v(" "+t._s(t.issue)+" ")]):t._e(),n("k-user-info",{attrs:{user:t.pending.email}}),n("k-text-field",{attrs:{autofocus:!0,counter:!1,help:t.$t("login.code.text."+t.pending.challenge),label:t.$t("login.code.label."+t.mode),novalidate:!0,placeholder:t.$t("login.code.placeholder."+t.pending.challenge),required:!0,autocomplete:"one-time-code",icon:"unlock",name:"code"},model:{value:t.code,callback:function(e){t.code=e},expression:"code"}}),n("div",{staticClass:"k-login-buttons"},[n("k-button",{staticClass:"k-login-button k-login-back-button",attrs:{icon:"angle-left"},on:{click:t.back}},[t._v(" "+t._s(t.$t("back"))+" "),t.isLoadingBack?[t._v(" … ")]:t._e()],2),n("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v(" "+t._s(t.$t("login"+("password-reset"===t.mode?".reset":"")))+" "),t.isLoadingLogin?[t._v(" … ")]:t._e()],2)],1)],1)}),[],!1,Gn,null,null,null);function Gn(t){for(let e in Kn)this[e]=Kn[e]}var Vn=function(){return Jn.exports}();const Wn={};var Xn=Rt({props:{display:{type:String,default:"HH:mm"},value:String},computed:{day(){return this.formatTimes([6,7,8,9,10,11,"-",12,13,14,15,16,17])},night(){return this.formatTimes([18,19,20,21,22,23,"-",0,1,2,3,4,5])}},methods:{formatTimes(t){return t.map((t=>{if("-"===t)return t;const e=this.$library.dayjs(t+":00","H:mm");return{display:e.format(this.display),select:e.toISO("time")}}))},select(t){this.$emit("input",t)}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-times"},[n("div",{staticClass:"k-times-slot"},[n("k-icon",{attrs:{type:"sun"}}),n("ul",t._l(t.day,(function(e){return n("li",{key:e.select},["-"===e?n("hr"):n("k-button",{on:{click:function(n){return t.select(e.select)}}},[t._v(t._s(e.display))])],1)})),0)],1),n("div",{staticClass:"k-times-slot"},[n("k-icon",{attrs:{type:"moon"}}),n("ul",t._l(t.night,(function(e){return n("li",{key:e.select},["-"===e?n("hr"):n("k-button",{on:{click:function(n){return t.select(e.select)}}},[t._v(t._s(e.display))])],1)})),0)],1)])}),[],!1,Zn,null,null,null);function Zn(t){for(let e in Wn)this[e]=Wn[e]}var Qn=function(){return Xn.exports}();const ts={props:{accept:{type:String,default:"*"},attributes:{type:Object},max:{type:Number},method:{type:String,default:"POST"},multiple:{type:Boolean,default:!0},url:{type:String}},data(){return{options:this.$props,completed:{},errors:[],files:[],total:0}},computed:{limit(){return!1===this.options.multiple?1:this.options.max}},methods:{open(t){this.params(t),setTimeout((()=>{this.$refs.input.click()}),1)},params(t){this.options=Object.assign({},this.$props,t)},select(t){this.upload(t.target.files)},drop(t,e){this.params(e),this.upload(t)},upload(t){this.$refs.dialog.open(),this.files=[...t],this.completed={},this.errors=[],this.hasErrors=!1,this.limit&&(this.files=this.files.slice(0,this.limit)),this.total=this.files.length,this.files.forEach((t=>{this.$helper.upload(t,{url:this.options.url,attributes:this.options.attributes,method:this.options.method,headers:{"X-CSRF":window.panel.$system.csrf},progress:(t,e,n)=>{var s,i;null==(i=null==(s=this.$refs[e.name])?void 0:s[0])||i.set(n)},success:(t,e,n)=>{this.complete(e,n.data)},error:(t,e,n)=>{this.errors.push({file:e,message:n.message}),this.complete(e,n.data)}})}))},complete(t,e){if(this.completed[t.name]=e,Object.keys(this.completed).length==this.total){if(this.$refs.input.value="",this.errors.length>0)return this.$forceUpdate(),void this.$emit("error",this.files);setTimeout((()=>{this.$refs.dialog.close(),this.$emit("success",this.files,Object.values(this.completed))}),250)}}}},es={};var ns=Rt(ts,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-upload"},[n("input",{ref:"input",attrs:{accept:t.options.accept,multiple:t.options.multiple,"aria-hidden":"true",type:"file",tabindex:"-1"},on:{change:t.select,click:function(t){t.stopPropagation()}}}),n("k-dialog",{ref:"dialog",staticClass:"k-upload-dialog",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"},scopedSlots:t._u([{key:"footer",fn:function(){return[t.errors.length>0?[n("k-button-group",{attrs:{buttons:[{icon:"check",text:t.$t("confirm"),click:function(){return t.$refs.dialog.close()}}]}})]:t._e()]},proxy:!0}])},[t.errors.length>0?[n("k-headline",[t._v(t._s(t.$t("upload.errors")))]),n("ul",{staticClass:"k-upload-error-list"},t._l(t.errors,(function(e,s){return n("li",{key:"error-"+s},[n("p",{staticClass:"k-upload-error-filename"},[t._v(" "+t._s(e.file.name)+" ")]),n("p",{staticClass:"k-upload-error-message"},[t._v(" "+t._s(e.message)+" ")])])})),0)]:[n("k-headline",[t._v(t._s(t.$t("upload.progress")))]),n("ul",{staticClass:"k-upload-list"},t._l(t.files,(function(e,s){return n("li",{key:"file-"+s},[n("k-progress",{ref:e.name,refInFor:!0}),n("p",{staticClass:"k-upload-list-filename"},[t._v(" "+t._s(e.name)+" ")]),n("p",[t._v(t._s(t.errors[e.name]))])],1)})),0)]],2)],1)}),[],!1,ss,null,null,null);function ss(t){for(let e in es)this[e]=es[e]}var is=function(){return ns.exports}();var os=t=>({$from:e})=>((t,e)=>{for(let n=t.depth;n>0;n--){const s=t.node(n);if(e(s))return{pos:n>0?t.before(n):0,start:t.start(n),depth:n,node:s}}})(e,t),rs=t=>e=>{if((t=>t instanceof b)(e)){const{node:n,$from:s}=e;if(((t,e)=>Array.isArray(t)&&t.indexOf(e.type)>-1||e.type===t)(t,n))return{node:n,pos:s.pos,depth:s.depth}}},as=(t,e,n={})=>{const s=rs(e)(t.selection)||os((t=>t.type===e))(t.selection);return Object.keys(n).length&&s?s.node.hasMarkup(e,a(a({},s.node.attrs),n)):!!s};function ls(t=null,e=null){if(!t||!e)return!1;const n=t.parent.childAfter(t.parentOffset);if(!n.node)return!1;const s=n.node.marks.find((t=>t.type===e));if(!s)return!1;let i=t.index(),o=t.start()+n.offset,r=i+1,a=o+n.node.nodeSize;for(;i>0&&s.isInSet(t.parent.child(i-1).marks);)i-=1,o-=t.parent.child(i).nodeSize;for(;r{i=[...i,...t.marks]}));const o=i.find((t=>t.type.name===e.name));return o?o.attrs:{}},getNodeAttrs:function(t,e){const{from:n,to:s}=t.selection;let i=[];t.doc.nodesBetween(n,s,(t=>{i=[...i,t]}));const o=i.reverse().find((t=>t.type.name===e.name));return o?o.attrs:{}},markInputRule:function(t,e,n){return new m(t,((t,s,i,o)=>{const r=n instanceof Function?n(s):n,{tr:a}=t,l=s.length-1;let u=o,c=i;if(s[l]){const n=i+s[0].indexOf(s[l-1]),r=n+s[l-1].length-1,d=n+s[l-1].lastIndexOf(s[l]),p=d+s[l].length,h=function(t,e,n){let s=[];return n.doc.nodesBetween(t,e,((t,e)=>{s=[...s,...t.marks.map((n=>({start:e,end:e+t.nodeSize,mark:n})))]})),s}(i,o,t).filter((t=>{const{excluded:n}=t.mark.type;return n.find((t=>t.name===e.name))})).filter((t=>t.end>n));if(h.length)return!1;pn&&a.delete(n,d),c=n,u=c+s[l].length}return a.addMark(c,u,e.create(r)),a.removeStoredMark(e),a}))},markIsActive:function(t,e){const{from:n,$from:s,to:i,empty:o}=t.selection;return o?!!e.isInSet(t.storedMarks||s.marks()):!!t.doc.rangeHasMark(n,i,e)},markPasteRule:function(t,e,n){const s=(i,o)=>{const r=[];return i.forEach((i=>{var a;if(i.isText){const{text:s,marks:l}=i;let u,c=0;const d=!!l.filter((t=>"link"===t.type.name))[0];for(;!d&&null!==(u=t.exec(s));)if((null==(a=null==o?void 0:o.type)?void 0:a.allowsMarkType(e))&&u[1]){const t=u.index,s=t+u[0].length,o=t+u[0].indexOf(u[1]),a=o+u[1].length,l=n instanceof Function?n(u):n;t>0&&r.push(i.cut(c,t)),r.push(i.cut(o,a).mark(e.create(l).addToSet(i.marks))),c=s}cnew k(s(t.content),t.openStart,t.openEnd)}})},minMax:function(t=0,e=0,n=0){return Math.min(Math.max(parseInt(t,10),e),n)},nodeIsActive:as,nodeInputRule:function(t,e,n){return new m(t,((t,s,i,o)=>{const r=n instanceof Function?n(s):n,{tr:a}=t;return s[0]&&a.replaceWith(i-1,o,e.create(r)),a}))},pasteRule:function(t,e,n){const s=i=>{const o=[];return i.forEach((i=>{if(i.isText){const{text:s}=i;let r,a=0;do{if(r=t.exec(s),r){const t=r.index,s=t+r[0].length,l=n instanceof Function?n(r[0]):n;t>0&&o.push(i.cut(a,t)),o.push(i.cut(t,s).mark(e.create(l).addToSet(i.marks))),a=s}}while(r);anew k(s(t.content),t.openStart,t.openEnd)}})},removeMark:function(t){return(e,n)=>{const{tr:s,selection:i}=e;let{from:o,to:r}=i;const{$from:a,empty:l}=i;if(l){const e=ls(a,t);o=e.from,r=e.to}return s.removeMark(o,r,t),n(s)}},toggleBlockType:function(t,e,n={}){return(s,i,o)=>as(s,t,n)?y(e)(s,i,o):y(t,n)(s,i,o)},toggleList:function(t,e){return(n,s,i)=>{const{schema:o,selection:r}=n,{$from:a,$to:l}=r,u=a.blockRange(l);if(!u)return!1;const c=os((t=>us(t,o)))(r);if(u.depth>=1&&c&&u.depth-c.depth<=1){if(c.node.type===t)return $(e)(n,s,i);if(us(c.node,o)&&t.validContent(c.node.content)){const{tr:e}=n;return e.setNodeMarkup(c.pos,t),s&&s(e),!1}}return _(t)(n,s,i)}},updateMark:function(t,e){return(n,s)=>{const{tr:i,selection:o,doc:r}=n,{ranges:a,empty:l}=o;if(l){const{from:n,to:s}=ls(o.$from,t);r.rangeHasMark(n,s,t)&&i.removeMark(n,s,t),i.addMark(n,s,t.create(e))}else a.forEach((n=>{const{$to:s,$from:o}=n;r.rangeHasMark(o.pos,s.pos,t)&&i.removeMark(o.pos,s.pos,t),i.addMark(o.pos,s.pos,t.create(e))}));return s(i)}}};class ds{constructor(t=[],e){t.forEach((t=>{t.bindEditor(e),t.init()})),this.extensions=t}commands({schema:t,view:e}){return this.extensions.filter((t=>t.commands)).reduce(((n,s)=>{const{name:i,type:o}=s,r={},l=s.commands(a({schema:t,utils:cs},["node","mark"].includes(o)?{type:t[`${o}s`][i]}:{})),u=(t,n)=>{r[t]=t=>{if("function"!=typeof n||!e.editable)return!1;e.focus();const s=n(t);return"function"==typeof s?s(e.state,e.dispatch,e):s}};return"object"==typeof l?Object.entries(l).forEach((([t,e])=>{u(t,e)})):u(i,l),a(a({},n),r)}),{})}buttons(t="mark"){const e={};return this.extensions.filter((e=>e.type===t)).filter((t=>t.button)).forEach((t=>{Array.isArray(t.button)?t.button.forEach((t=>{e[t.id||t.name]=t})):e[t.name]=t.button})),e}getAllowedExtensions(t){return t instanceof Array||!t?t instanceof Array?this.extensions.filter((e=>!t.includes(e.name))):this.extensions:[]}getFromExtensions(t,e,n=this.extensions){return n.filter((t=>["extension"].includes(t.type))).filter((e=>e[t])).map((n=>n[t](l(a({},e),{utils:cs}))))}getFromNodesAndMarks(t,e,n=this.extensions){return n.filter((t=>["node","mark"].includes(t.type))).filter((e=>e[t])).map((n=>n[t](l(a({},e),{type:e.schema[`${n.type}s`][n.name],utils:cs}))))}inputRules({schema:t,excludedExtensions:e}){const n=this.getAllowedExtensions(e);return[...this.getFromExtensions("inputRules",{schema:t},n),...this.getFromNodesAndMarks("inputRules",{schema:t},n)].reduce(((t,e)=>[...t,...e]),[])}keymaps({schema:t}){return[...this.getFromExtensions("keys",{schema:t}),...this.getFromNodesAndMarks("keys",{schema:t})].map((t=>M(t)))}get marks(){return this.extensions.filter((t=>"mark"===t.type)).reduce(((t,{name:e,schema:n})=>l(a({},t),{[e]:n})),{})}get nodes(){return this.extensions.filter((t=>"node"===t.type)).reduce(((t,{name:e,schema:n})=>l(a({},t),{[e]:n})),{})}get options(){const{view:t}=this;return this.extensions.reduce(((e,n)=>l(a({},e),{[n.name]:new Proxy(n.options,{set(e,n,s){const i=e[n]!==s;return Object.assign(e,{[n]:s}),i&&t.updateState(t.state),!0}})})),{})}pasteRules({schema:t,excludedExtensions:e}){const n=this.getAllowedExtensions(e);return[...this.getFromExtensions("pasteRules",{schema:t},n),...this.getFromNodesAndMarks("pasteRules",{schema:t},n)].reduce(((t,e)=>[...t,...e]),[])}plugins({schema:t}){return[...this.getFromExtensions("plugins",{schema:t}),...this.getFromNodesAndMarks("plugins",{schema:t})].reduce(((t,e)=>[...t,...e]),[]).map((t=>t instanceof g?t:new g(t)))}}class ps{constructor(t={}){this.options=a(a({},this.defaults),t)}init(){return null}bindEditor(t=null){this.editor=t}get name(){return null}get type(){return"extension"}get defaults(){return{}}plugins(){return[]}inputRules(){return[]}pasteRules(){return[]}keys(){return{}}}class hs extends ps{constructor(t={}){super(t)}get type(){return"node"}get schema(){return null}commands(){return{}}}class fs extends hs{get defaults(){return{inline:!1}}get name(){return"doc"}get schema(){return{content:this.options.inline?"paragraph+":"block+"}}}class ms extends hs{get button(){return{id:this.name,icon:"paragraph",label:window.panel.$t("toolbar.button.paragraph"),name:this.name}}commands({utils:t,type:e}){return{paragraph:()=>t.setBlockType(e)}}get schema(){return{content:"inline*",group:"block",draggable:!1,parseDOM:[{tag:"p"}],toDOM:()=>["p",0]}}get name(){return"paragraph"}}class gs extends hs{get name(){return"text"}get schema(){return{group:"inline"}}}class ks extends class{emit(t,...e){this._callbacks=this._callbacks||{};const n=this._callbacks[t];return n&&n.forEach((t=>t.apply(this,e))),this}off(t,e){if(arguments.length){const n=this._callbacks?this._callbacks[t]:null;n&&(e?this._callbacks[t]=n.filter((t=>t!==e)):delete this._callbacks[t])}else this._callbacks={};return this}on(t,e){return this._callbacks=this._callbacks||{},this._callbacks[t]=this._callbacks[t]||[],this._callbacks[t].push(e),this}}{constructor(t={}){super(),this.defaults={autofocus:!1,content:"",disableInputRules:!1,disablePasteRules:!1,editable:!0,element:null,extensions:[],emptyDocument:{type:"doc",content:[]},events:{},inline:!1,parseOptions:{},topNode:"doc",useBuiltInExtensions:!0},this.init(t)}blur(){this.view.dom.blur()}get builtInExtensions(){return this.options.useBuiltInExtensions?[new fs({inline:this.options.inline}),new gs,new ms]:[]}buttons(t){return this.extensions.buttons(t)}clearContent(t=!1){this.setContent(this.options.emptyDocument,t)}command(t,...e){this.commands[t]&&this.commands[t](...e)}createCommands(){return this.extensions.commands({schema:this.schema,view:this.view})}createDocument(t,e=this.options.parseOptions){if(null===t)return this.schema.nodeFromJSON(this.options.emptyDocument);if("object"==typeof t)try{return this.schema.nodeFromJSON(t)}catch(n){return window.console.warn("Invalid content.","Passed value:",t,"Error:",n),this.schema.nodeFromJSON(this.options.emptyDocument)}if("string"==typeof t){const n=`
${t}
`,s=(new window.DOMParser).parseFromString(n,"text/html").body.firstElementChild;return I.fromSchema(this.schema).parse(s,e)}return!1}createEvents(){const t=this.options.events||{};return Object.entries(t).forEach((([t,e])=>{this.on(t,e)})),t}createExtensions(){return new ds([...this.builtInExtensions,...this.options.extensions],this)}createFocusEvents(){const t=(t,e,n=!0)=>{this.focused=n,this.emit(n?"focus":"blur",{event:e,state:t.state,view:t});const s=this.state.tr.setMeta("focused",n);this.view.dispatch(s)};return new g({props:{attributes:{tabindex:0},handleDOMEvents:{focus:(e,n)=>{t(e,n,!0)},blur:(e,n)=>{t(e,n,!1)}}}})}createInputRules(){return this.extensions.inputRules({schema:this.schema,excludedExtensions:this.options.disableInputRules})}createKeymaps(){return this.extensions.keymaps({schema:this.schema})}createMarks(){return this.extensions.marks}createNodes(){return this.extensions.nodes}createPasteRules(){return this.extensions.pasteRules({schema:this.schema,excludedExtensions:this.options.disablePasteRules})}createPlugins(){return this.extensions.plugins({schema:this.schema})}createSchema(){return new L({topNode:this.options.topNode,nodes:this.nodes,marks:this.marks})}createState(){return j.create({schema:this.schema,doc:this.createDocument(this.options.content),plugins:[...this.plugins,D({rules:this.inputRules}),...this.pasteRules,...this.keymaps,M({Backspace:q}),M(R),this.createFocusEvents()]})}createView(){return new B(this.element,{dispatchTransaction:this.dispatchTransaction.bind(this),editable:()=>this.options.editable,handlePaste:(t,e)=>{if("function"==typeof this.events.paste){const t=e.clipboardData.getData("text/html"),n=e.clipboardData.getData("text/plain");if(!0===this.events.paste(e,t,n))return!0}},handleDrop:(...t)=>{this.emit("drop",...t)},state:this.createState()})}destroy(){this.view&&this.view.destroy()}dispatchTransaction(t){const e=this.state,n=this.state.apply(t);this.view.updateState(n),this.selection={from:this.state.selection.from,to:this.state.selection.to},this.setActiveNodesAndMarks();const s={editor:this,getHTML:this.getHTML.bind(this),getJSON:this.getJSON.bind(this),state:this.state,transaction:t};this.emit("transaction",s),!t.docChanged&&t.getMeta("preventUpdate")||this.emit("update",s);const{from:i,to:o}=this.state.selection,r=!e||!e.selection.eq(n.selection);this.emit(n.selection.empty?"deselect":"select",l(a({},s),{from:i,hasChanged:r,to:o}))}focus(t=null){if(this.view.focused&&null===t||!1===t)return;const{from:e,to:n}=this.selectionAtPosition(t);this.setSelection(e,n),setTimeout((()=>this.view.focus()),10)}getHTML(){const t=document.createElement("div"),e=P.fromSchema(this.schema).serializeFragment(this.state.doc.content);return t.appendChild(e),this.options.inline&&t.querySelector("p")?t.querySelector("p").innerHTML:t.innerHTML}getJSON(){return this.state.doc.toJSON()}getMarkAttrs(t=null){return this.activeMarkAttrs[t]}getSchemaJSON(){return JSON.parse(JSON.stringify({nodes:this.nodes,marks:this.marks}))}init(t={}){this.options=a(a({},this.defaults),t),this.element=this.options.element,this.focused=!1,this.selection={from:0,to:0},this.events=this.createEvents(),this.extensions=this.createExtensions(),this.nodes=this.createNodes(),this.marks=this.createMarks(),this.schema=this.createSchema(),this.keymaps=this.createKeymaps(),this.inputRules=this.createInputRules(),this.pasteRules=this.createPasteRules(),this.plugins=this.createPlugins(),this.view=this.createView(),this.commands=this.createCommands(),this.setActiveNodesAndMarks(),!1!==this.options.autofocus&&this.focus(this.options.autofocus),this.emit("init",{view:this.view,state:this.state}),this.extensions.view=this.view}isEditable(){return this.options.editable}isEmpty(){if(this.state)return 0===this.state.doc.textContent.length}get isActive(){return Object.entries(a(a({},this.activeMarks),this.activeNodes)).reduce(((t,[e,n])=>l(a({},t),{[e]:(t={})=>n(t)})),{})}removeMark(t){if(this.schema.marks[t])return cs.removeMark(this.schema.marks[t])(this.state,this.view.dispatch)}selectionAtPosition(t=null){if(this.selection&&null===t)return this.selection;if("start"===t||!0===t)return{from:0,to:0};if("end"===t){const{doc:t}=this.state;return{from:t.content.size,to:t.content.size}}return{from:t,to:t}}setActiveNodesAndMarks(){this.activeMarks=Object.values(this.schema.marks).filter((t=>cs.markIsActive(this.state,t))).map((t=>t.name)),this.activeMarkAttrs=Object.entries(this.schema.marks).reduce(((t,[e,n])=>l(a({},t),{[e]:cs.getMarkAttrs(this.state,n)})),{}),this.activeNodes=Object.values(this.schema.nodes).filter((t=>cs.nodeIsActive(this.state,t))).map((t=>t.name)),this.activeNodeAttrs=Object.entries(this.schema.nodes).reduce(((t,[e,n])=>l(a({},t),{[e]:cs.getNodeAttrs(this.state,n)})),{})}setContent(t={},e=!1,n){const{doc:s,tr:i}=this.state,o=this.createDocument(t,n),r=N.create(s,0,s.content.size),a=i.setSelection(r).replaceSelectionWith(o,!1).setMeta("preventUpdate",!e);this.view.dispatch(a)}setSelection(t=0,e=0){const{doc:n,tr:s}=this.state,i=cs.minMax(t,0,n.content.size),o=cs.minMax(e,0,n.content.size),r=N.create(n,i,o),a=s.setSelection(r);this.view.dispatch(a)}get state(){return this.view?this.view.state:null}toggleMark(t){if(this.schema.marks[t])return cs.toggleMark(this.schema.marks[t])(this.state,this.view.dispatch)}updateMark(t,e){if(this.schema.marks[t])return cs.updateMark(this.schema.marks[t],e)(this.state,this.view.dispatch)}}const vs={};var bs=Rt({data:()=>({link:{href:null,title:null,target:!1}}),computed:{fields(){return{href:{label:this.$t("url"),type:"text",icon:"url"},title:{label:this.$t("title"),type:"text",icon:"title"},target:{label:this.$t("open.newWindow"),type:"toggle",text:[this.$t("no"),this.$t("yes")]}}}},methods:{open(t){this.link=a({title:null,target:!1},t),this.link.target=Boolean(this.link.target),this.$refs.dialog.open()},submit(){this.$emit("submit",l(a({},this.link),{target:this.link.target?"_blank":null})),this.$refs.dialog.close()}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("confirm"),size:"medium"},on:{close:function(e){return t.$emit("close")},submit:t.submit},model:{value:t.link,callback:function(e){t.link=e},expression:"link"}})}),[],!1,ys,null,null,null);function ys(t){for(let e in vs)this[e]=vs[e]}var $s=function(){return bs.exports}();const _s={};var ws=Rt({data:()=>({email:{email:null,title:null}}),computed:{fields(){return{href:{label:this.$t("email"),type:"email",icon:"email"},title:{label:this.$t("title"),type:"text",icon:"title"}}}},methods:{open(t){this.email=a({title:null},t),this.$refs.dialog.open()},submit(){this.$emit("submit",this.email),this.$refs.dialog.close()}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("confirm"),size:"medium"},on:{close:function(e){return t.$emit("close")},submit:t.submit},model:{value:t.email,callback:function(e){t.email=e},expression:"email"}})}),[],!1,xs,null,null,null);function xs(t){for(let e in _s)this[e]=_s[e]}var Ss=function(){return ws.exports}();class Cs extends ps{constructor(t={}){super(t)}command(){return()=>{}}remove(){this.editor.removeMark(this.name)}get schema(){return null}get type(){return"mark"}toggle(){return this.editor.toggleMark(this.name)}update(t){this.editor.updateMark(this.name,t)}}class Os extends Cs{get button(){return{icon:"code",label:window.panel.$t("toolbar.button.code")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/(?:`)([^`]+)(?:`)$/,t)]}keys(){return{"Mod-`":()=>this.toggle()}}get name(){return"code"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/(?:`)([^`]+)(?:`)/g,t)]}get schema(){return{excludes:"_",parseDOM:[{tag:"code"}],toDOM:()=>["code",0]}}}class Es extends Cs{get button(){return{icon:"bold",label:window.panel.$t("toolbar.button.bold")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/,t)]}keys(){return{"Mod-b":()=>this.toggle()}}get name(){return"bold"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)/g,t)]}get schema(){return{parseDOM:[{tag:"strong"},{tag:"b",getAttrs:t=>"normal"!==t.style.fontWeight&&null},{style:"font-weight",getAttrs:t=>/^(bold(er)?|[5-9]\d{2,})$/.test(t)&&null}],toDOM:()=>["strong",0]}}}class Ts extends Cs{get button(){return{icon:"italic",label:window.panel.$t("toolbar.button.italic")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/,t),e.markInputRule(/(?:^|\s)((?:_)((?:[^_]+))(?:_))$/,t)]}keys(){return{"Mod-i":()=>this.toggle()}}get name(){return"italic"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/_([^_]+)_/g,t),e.markPasteRule(/\*([^*]+)\*/g,t)]}get schema(){return{parseDOM:[{tag:"i"},{tag:"em"},{style:"font-style=italic"}],toDOM:()=>["em",0]}}}class As extends Cs{get button(){return{icon:"url",label:window.panel.$t("toolbar.button.link")}}commands(){return{link:()=>{this.editor.emit("link",this.editor)},insertLink:(t={})=>{if(t.href)return this.update(t)},removeLink:()=>this.remove(),toggleLink:(t={})=>{var e;(null==(e=t.href)?void 0:e.length)>0?this.editor.command("insertLink",t):this.editor.command("removeLink")}}}get defaults(){return{target:null}}get name(){return"link"}pasteRules({type:t,utils:e}){return[e.pasteRule(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b([-a-zA-Z0-9@:%_+.~#?&//=,]*)/gi,t,(t=>({href:t})))]}plugins(){return[{props:{handleClick:(t,e,n)=>{const s=this.editor.getMarkAttrs("link");s.href&&!0===n.altKey&&n.target instanceof HTMLAnchorElement&&(n.stopPropagation(),window.open(s.href,s.target))}}}]}get schema(){return{attrs:{href:{default:null},target:{default:null},title:{default:null}},inclusive:!1,parseDOM:[{tag:"a[href]:not([href^='mailto:'])",getAttrs:t=>({href:t.getAttribute("href"),target:t.getAttribute("target"),title:t.getAttribute("title")})}],toDOM:t=>["a",l(a({},t.attrs),{rel:"noopener noreferrer"}),0]}}}class Ms extends Cs{get button(){return{icon:"email",label:"Email"}}commands(){return{email:()=>{this.editor.emit("email")},insertEmail:(t={})=>{if(t.href)return this.update(t)},removeEmail:()=>this.remove(),toggleEmail:(t={})=>{var e;(null==(e=t.href)?void 0:e.length)>0?this.editor.command("insertEmail",t):this.editor.command("removeEmail")}}}get defaults(){return{target:null}}get name(){return"email"}pasteRules({type:t,utils:e}){return[e.pasteRule(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/gi,t,(t=>({href:t})))]}plugins(){return[{props:{handleClick:(t,e,n)=>{const s=this.editor.getMarkAttrs("email");s.href&&!0===n.altKey&&n.target instanceof HTMLAnchorElement&&(n.stopPropagation(),window.open(s.href))}}}]}get schema(){return{attrs:{href:{default:null},title:{default:null}},inclusive:!1,parseDOM:[{tag:"a[href^='mailto:']",getAttrs:t=>({href:t.getAttribute("href").replace("mailto:",""),title:t.getAttribute("title")})}],toDOM:t=>["a",l(a({},t.attrs),{href:"mailto:"+t.attrs.href}),0]}}}class Is extends Cs{get button(){return{icon:"strikethrough",label:window.panel.$t("toolbar.button.strike")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/~([^~]+)~$/,t)]}keys(){return{"Mod-d":()=>this.toggle()}}get name(){return"strike"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/~([^~]+)~/g,t)]}get schema(){return{parseDOM:[{tag:"s"},{tag:"del"},{tag:"strike"},{style:"text-decoration",getAttrs:t=>"line-through"===t}],toDOM:()=>["s",0]}}}class Ls extends Cs{get button(){return{icon:"underline",label:window.panel.$t("toolbar.button.underline")}}commands(){return()=>this.toggle()}keys(){return{"Mod-u":()=>this.toggle()}}get name(){return"underline"}get schema(){return{parseDOM:[{tag:"u"},{style:"text-decoration",getAttrs:t=>"underline"===t}],toDOM:()=>["u",0]}}}class js extends hs{get button(){return{id:this.name,icon:"list-bullet",label:window.panel.$t("toolbar.button.ul"),name:this.name,when:["listItem","bulletList","orderedList"]}}commands({type:t,schema:e,utils:n}){return()=>n.toggleList(t,e.nodes.listItem)}inputRules({type:t,utils:e}){return[e.wrappingInputRule(/^\s*([-+*])\s$/,t)]}keys({type:t,schema:e,utils:n}){return{"Shift-Ctrl-8":n.toggleList(t,e.nodes.listItem)}}get name(){return"bulletList"}get schema(){return{content:"listItem+",group:"block",parseDOM:[{tag:"ul"}],toDOM:()=>["ul",0]}}}class Ds extends hs{commands({utils:t,type:e}){return()=>this.createHardBreak(t,e)}createHardBreak(t,e){return t.chainCommands(t.exitCode,((t,n)=>(n(t.tr.replaceSelectionWith(e.create()).scrollIntoView()),!0)))}get defaults(){return{enter:!1,text:!1}}keys({utils:t,type:e}){const n=this.createHardBreak(t,e);let s={"Mod-Enter":n,"Shift-Enter":n};return this.options.enter&&(s.Enter=n),s}get name(){return"hardBreak"}get schema(){return{inline:!0,group:"inline",selectable:!1,parseDOM:[{tag:"br"}],toDOM:()=>["br"]}}}class Bs extends hs{get button(){return this.options.levels.map((t=>({id:`h${t}`,command:`h${t}`,icon:`h${t}`,label:window.panel.$t("toolbar.button.heading."+t),attrs:{level:t},name:this.name,when:["heading","paragraph"]})))}commands({type:t,schema:e,utils:n}){let s={toggleHeading:s=>n.toggleBlockType(t,e.nodes.paragraph,s)};return this.options.levels.forEach((i=>{s[`h${i}`]=()=>n.toggleBlockType(t,e.nodes.paragraph,{level:i})})),s}get defaults(){return{levels:[1,2,3,4,5,6]}}inputRules({type:t,utils:e}){return this.options.levels.map((n=>e.textblockTypeInputRule(new RegExp(`^(#{1,${n}})\\s$`),t,(()=>({level:n})))))}keys({type:t,utils:e}){return this.options.levels.reduce(((n,s)=>a(a({},n),{[`Shift-Ctrl-${s}`]:e.setBlockType(t,{level:s})})),{})}get name(){return"heading"}get schema(){return{attrs:{level:{default:1}},content:"inline*",group:"block",defining:!0,draggable:!1,parseDOM:this.options.levels.map((t=>({tag:`h${t}`,attrs:{level:t}}))),toDOM:t=>[`h${t.attrs.level}`,0]}}}class Ps extends hs{commands({type:t}){return()=>(e,n)=>n(e.tr.replaceSelectionWith(t.create()))}inputRules({type:t,utils:e}){return[e.nodeInputRule(/^(?:---|___\s|\*\*\*\s)$/,t)]}get name(){return"horizontalRule"}get schema(){return{group:"block",parseDOM:[{tag:"hr"}],toDOM:()=>["hr"]}}}class Ns extends hs{keys({type:t,utils:e}){return{Enter:e.splitListItem(t),"Shift-Tab":e.liftListItem(t),Tab:e.sinkListItem(t)}}get name(){return"listItem"}get schema(){return{content:"paragraph block*",defining:!0,draggable:!1,parseDOM:[{tag:"li"}],toDOM:()=>["li",0]}}}class qs extends hs{get button(){return{id:this.name,icon:"list-numbers",label:window.panel.$t("toolbar.button.ol"),name:this.name,when:["listItem","bulletList","orderedList"]}}commands({type:t,schema:e,utils:n}){return()=>n.toggleList(t,e.nodes.listItem)}inputRules({type:t,utils:e}){return[e.wrappingInputRule(/^(\d+)\.\s$/,t,(t=>({order:+t[1]})),((t,e)=>e.childCount+e.attrs.order===+t[1]))]}keys({type:t,schema:e,utils:n}){return{"Shift-Ctrl-9":n.toggleList(t,e.nodes.listItem)}}get name(){return"orderedList"}get schema(){return{attrs:{order:{default:1}},content:"listItem+",group:"block",parseDOM:[{tag:"ol",getAttrs:t=>({order:t.hasAttribute("start")?+t.getAttribute("start"):1})}],toDOM:t=>1===t.attrs.order?["ol",0]:["ol",{start:t.attrs.order},0]}}}class Rs extends ps{commands(){return{undo:()=>F,redo:()=>z,undoDepth:()=>Y,redoDepth:()=>H}}get defaults(){return{depth:"",newGroupDelay:""}}keys(){return{"Mod-z":F,"Mod-y":z,"Shift-Mod-z":z,"Mod-я":F,"Shift-Mod-я":z}}get name(){return"history"}plugins(){return[U({depth:this.options.depth,newGroupDelay:this.options.newGroupDelay})]}}class Fs extends ps{commands(){return{insertHtml:t=>(e,n)=>{let s=document.createElement("div");s.innerHTML=t.trim();const i=I.fromSchema(e.schema).parse(s);n(e.tr.replaceSelectionWith(i).scrollIntoView())}}}}class zs extends ps{constructor(t={}){super(t)}close(){this.visible=!1,this.emit()}emit(){this.editor.emit("toolbar",{marks:this.marks,nodes:this.nodes,nodeAttrs:this.nodeAttrs,position:this.position,visible:this.visible})}init(){this.position={left:0,bottom:0},this.visible=!1,this.editor.on("blur",(()=>{this.close()})),this.editor.on("deselect",(()=>{this.close()})),this.editor.on("select",(({hasChanged:t})=>{!1!==t?this.open():this.emit()}))}get marks(){return this.editor.activeMarks}get nodes(){return this.editor.activeNodes}get nodeAttrs(){return this.editor.activeNodeAttrs}open(){this.visible=!0,this.reposition(),this.emit()}reposition(){const{from:t,to:e}=this.editor.selection,n=this.editor.view.coordsAtPos(t),s=this.editor.view.coordsAtPos(e,!0),i=this.editor.element.getBoundingClientRect();let o=(n.left+s.left)/2-i.left,r=Math.round(i.bottom-n.top);return this.position={bottom:r,left:o}}get type(){return"toolbar"}}const Ys={props:{activeMarks:{type:Array,default:()=>[]},activeNodes:{type:Array,default:()=>[]},activeNodeAttrs:{type:[Array,Object],default:()=>[]},editor:{type:Object,required:!0},marks:{type:Array},isParagraphNodeHidden:{type:Boolean,default:!1}},computed:{activeButton(){return Object.values(this.nodeButtons).find((t=>this.isButtonActive(t)))||!1},hasVisibleButtons(){const t=Object.keys(this.nodeButtons);return t.length>1||1===t.length&&!1===t.includes("paragraph")},markButtons(){return this.buttons("mark")},nodeButtons(){let t=this.buttons("node");return!0===this.isParagraphNodeHidden&&t.paragraph&&delete t.paragraph,t}},methods:{buttons(t){const e=this.editor.buttons(t);let n=this.sorting;!1!==n&&!1!==Array.isArray(n)||(n=Object.keys(e));let s={};return n.forEach((t=>{e[t]&&(s[t]=e[t])})),s},command(t,...e){this.$emit("command",t,...e)},isButtonActive(t){if("paragraph"===t.name)return 1===this.activeNodes.length&&this.activeNodes.includes(t.name);let e=!0;if(t.attrs){const n=Object.values(this.activeNodeAttrs).find((e=>JSON.stringify(e)===JSON.stringify(t.attrs)));e=Boolean(n||!1)}return!0===e&&this.activeNodes.includes(t.name)},isButtonCurrent(t){return!!this.activeButton&&this.activeButton.id===t.id},isButtonDisabled(t){var e;if(null==(e=this.activeButton)?void 0:e.when){return!1===this.activeButton.when.includes(t.name)}return!1},needDividerAfterNode(t){let e=["paragraph"],n=Object.keys(this.nodeButtons);return(n.includes("bulletList")||n.includes("orderedList"))&&e.push("h6"),e.includes(t.id)}}},Hs={};var Us=Rt(Ys,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-writer-toolbar"},[t.hasVisibleButtons?n("k-dropdown",{nativeOn:{mousedown:function(t){t.preventDefault()}}},[n("k-button",{class:{"k-writer-toolbar-button k-writer-toolbar-nodes":!0,"k-writer-toolbar-button-active":!!t.activeButton},attrs:{icon:t.activeButton.icon||"title"},on:{click:function(e){return t.$refs.nodes.toggle()}}}),n("k-dropdown-content",{ref:"nodes"},[t._l(t.nodeButtons,(function(e,s){return[n("k-dropdown-item",{key:s,attrs:{current:t.isButtonCurrent(e),disabled:t.isButtonDisabled(e),icon:e.icon},on:{click:function(n){return t.command(e.command||s)}}},[t._v(" "+t._s(e.label)+" ")]),t.needDividerAfterNode(e)?n("hr",{key:s+"-divider"}):t._e()]}))],2)],1):t._e(),t._l(t.markButtons,(function(e,s){return n("k-button",{key:s,class:{"k-writer-toolbar-button":!0,"k-writer-toolbar-button-active":t.activeMarks.includes(s)},attrs:{icon:e.icon,tooltip:e.label},on:{mousedown:function(n){return n.preventDefault(),t.command(e.command||s)}}})}))],2)}),[],!1,Ks,null,null,null);function Ks(t){for(let e in Hs)this[e]=Hs[e]}const Js={props:{autofocus:Boolean,breaks:Boolean,code:Boolean,disabled:Boolean,emptyDocument:{type:Object,default:()=>({type:"doc",content:[]})},headings:[Array,Boolean],inline:{type:Boolean,default:!1},marks:{type:[Array,Boolean],default:!0},nodes:{type:[Array,Boolean],default:()=>["heading","bulletList","orderedList"]},paste:{type:Function,default:()=>()=>!1},placeholder:String,spellcheck:Boolean,extensions:Array,value:{type:String,default:""}}},Gs={};var Vs=Rt({components:{"k-writer-email-dialog":Ss,"k-writer-link-dialog":$s,"k-writer-toolbar":function(){return Us.exports}()},mixins:[Js],data(){return{editor:null,json:{},html:this.value,isEmpty:!0,toolbar:!1}},computed:{isParagraphNodeHidden(){return!0===Array.isArray(this.nodes)&&3!==this.nodes.length&&!1===this.nodes.includes("paragraph")}},watch:{value(t,e){t!==e&&t!==this.html&&(this.html=t,this.editor.setContent(this.html))}},mounted(){this.editor=new ks({autofocus:this.autofocus,content:this.value,editable:!this.disabled,element:this.$el,emptyDocument:this.emptyDocument,events:{link:t=>{this.$refs.linkDialog.open(t.getMarkAttrs("link"))},email:()=>{this.$refs.emailDialog.open(this.editor.getMarkAttrs("email"))},paste:this.paste,toolbar:t=>{this.toolbar=t,this.toolbar.visible&&this.$nextTick((()=>{this.onToolbarOpen()}))},update:t=>{const e=JSON.stringify(this.editor.getJSON());e!==JSON.stringify(this.json)&&(this.json=e,this.isEmpty=t.editor.isEmpty(),this.html=t.editor.getHTML(),this.isEmpty&&(0===t.editor.activeNodes.length||t.editor.activeNodes.includes("paragraph"))&&(this.html=""),this.$emit("input",this.html))}},extensions:[...this.createMarks(),...this.createNodes(),new Rs,new Fs,new zs,...this.extensions||[]],inline:this.inline}),this.isEmpty=this.editor.isEmpty(),this.json=this.editor.getJSON()},beforeDestroy(){this.editor.destroy()},methods:{filterExtensions(t,e,n){!1===e?e=[]:!0!==e&&!1!==Array.isArray(e)||(e=Object.keys(t));let s=[];return e.forEach((e=>{t[e]&&s.push(t[e])})),"function"==typeof n&&(s=n(e,s)),s},command(t,...e){this.editor.command(t,...e)},createMarks(){return this.filterExtensions({bold:new Es,italic:new Ts,strike:new Is,underline:new Ls,code:new Os,link:new As,email:new Ms},this.marks)},createNodes(){const t=new Ds({text:!0,enter:this.inline});return!0===this.inline?[t]:this.filterExtensions({bulletList:new js,orderedList:new qs,heading:new Bs,horizontalRule:new Ps,listItem:new Ns},this.nodes,((e,n)=>((e.includes("bulletList")||e.includes("orderedList"))&&n.push(new Ns),n.push(t),n)))},getHTML(){return this.editor.getHTML()},focus(){this.editor.focus()},onToolbarOpen(){if(this.$refs.toolbar){const t=this.$el.clientWidth,e=this.$refs.toolbar.$el.clientWidth;let n=this.toolbar.position.left;n-e/2<0&&(n=n+(e/2-n)-20),n+e/2>t&&(n=n-(n+e/2-t)+20),n!==this.toolbar.position.left&&(this.$refs.toolbar.$el.style.left=n+"px")}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{directives:[{name:"direction",rawName:"v-direction"}],ref:"editor",staticClass:"k-writer",attrs:{"data-empty":t.isEmpty,"data-placeholder":t.placeholder,spellcheck:t.spellcheck}},[t.editor?[t.toolbar.visible?n("k-writer-toolbar",{ref:"toolbar",style:{bottom:t.toolbar.position.bottom+"px","inset-inline-start":t.toolbar.position.left+"px"},attrs:{editor:t.editor,"active-marks":t.toolbar.marks,"active-nodes":t.toolbar.nodes,"active-node-attrs":t.toolbar.nodeAttrs,"is-paragraph-node-hidden":t.isParagraphNodeHidden},on:{command:function(e){return t.editor.command(e)}}}):t._e(),n("k-writer-link-dialog",{ref:"linkDialog",on:{close:function(e){return t.editor.focus()},submit:function(e){return t.editor.command("toggleLink",e)}}}),n("k-writer-email-dialog",{ref:"emailDialog",on:{close:function(e){return t.editor.focus()},submit:function(e){return t.editor.command("toggleEmail",e)}}})]:t._e()],2)}),[],!1,Ws,null,null,null);function Ws(t){for(let e in Gs)this[e]=Gs[e]}var Xs=function(){return Vs.exports}();const Zs={};var Qs=Rt({},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-login-alert",on:{click:function(e){return t.$emit("click")}}},[n("span",[t._t("default")],2),n("k-icon",{attrs:{type:"alert"}})],1)}),[],!1,ti,null,null,null);function ti(t){for(let e in Zs)this[e]=Zs[e]}var ei=function(){return Qs.exports}();const ni={mixins:[vn,yn,_n,xn,Cn],inheritAttrs:!1,props:{value:Boolean},watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},onChange(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()}},validations(){return{value:{required:!this.required||K.required}}}},si={};var ii=Rt(ni,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-checkbox-input",on:{click:function(t){t.stopPropagation()}}},[n("input",{ref:"input",staticClass:"k-checkbox-input-native",attrs:{id:t.id,disabled:t.disabled,type:"checkbox"},domProps:{checked:t.value},on:{change:function(e){return t.onChange(e.target.checked)}}}),n("span",{staticClass:"k-checkbox-input-icon",attrs:{"aria-hidden":"true"}},[n("svg",{attrs:{width:"12",height:"10",viewBox:"0 0 12 10",xmlns:"http://www.w3.org/2000/svg"}},[n("path",{attrs:{d:"M1 5l3.3 3L11 1","stroke-width":"2",fill:"none","fill-rule":"evenodd"}})])]),n("span",{staticClass:"k-checkbox-input-label",domProps:{innerHTML:t._s(t.label)}})])}),[],!1,oi,null,null,null);function oi(t){for(let e in si)this[e]=si[e]}var ri=function(){return ii.exports}();const ai={mixins:[vn,yn,_n,Cn],props:{columns:Number,max:Number,min:Number,options:Array,value:{type:[Array,Object],default:()=>[]}}},li={};var ui=Rt({mixins:[ai],inheritAttrs:!1,data(){return{selected:this.valueToArray(this.value)}},watch:{value(t){this.selected=this.valueToArray(t)},selected(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$el.querySelector("input").focus()},onInput(t,e){if(!0===e)this.selected.push(t);else{const e=this.selected.indexOf(t);-1!==e&&this.selected.splice(e,1)}this.$emit("input",this.selected)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()},valueToArray:t=>!0===Array.isArray(t)?t:"string"==typeof t?String(t).split(","):"object"==typeof t?Object.values(t):void 0},validations(){return{selected:{required:!this.required||K.required,min:!this.min||K.minLength(this.min),max:!this.max||K.maxLength(this.max)}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ul",{staticClass:"k-checkboxes-input",style:"--columns:"+t.columns},t._l(t.options,(function(e,s){return n("li",{key:s},[n("k-checkbox-input",{attrs:{id:t.id+"-"+s,label:e.text,value:-1!==t.selected.indexOf(e.value)},on:{input:function(n){return t.onInput(e.value,n)}}})],1)})),0)}),[],!1,ci,null,null,null);function ci(t){for(let e in li)this[e]=li[e]}var di=function(){return ui.exports}();const pi={mixins:[vn,yn,_n,Cn],props:{display:{type:String,default:"DD.MM.YYYY"},max:String,min:String,step:{type:Object,default:()=>({size:1,unit:"day"})},type:{type:String,default:"date"},value:String}},hi={};var fi=Rt({mixins:[pi],inheritAttrs:!1,data:()=>({dt:null,formatted:null}),computed:{inputType:()=>"date",pattern(){return this.$library.dayjs.pattern(this.display)},rounding(){return a(a({},this.$options.props.step.default()),this.step)}},watch:{value:{handler(t,e){if(t!==e){const e=this.toDatetime(t);this.commit(e)}},immediate:!0}},created(){this.$events.$on("keydown.cmd.s",this.onBlur)},destroyed(){this.$events.$off("keydown.cmd.s",this.onBlur)},methods:{alter(t){let e=this.parse()||this.round(this.$library.dayjs()),n=this.rounding.unit,s=this.rounding.size;const i=this.selection();null!==i&&("meridiem"===i.unit?(t="pm"===e.format("a")?"subtract":"add",n="hour",s=12):(n=i.unit,n!==this.rounding.unit&&(s=1))),e=e[t](s,n).round(this.rounding.unit,this.rounding.size),this.commit(e),this.emit(e),this.$nextTick((()=>this.select(i)))},commit(t){this.dt=t,this.formatted=this.pattern.format(t),this.$emit("invalid",this.$v.$invalid,this.$v)},emit(t){this.$emit("input",this.toISO(t))},focus(){this.$refs.input.focus()},onArrowDown(){this.alter("subtract")},onArrowUp(){this.alter("add")},onBlur(){const t=this.parse();this.commit(t),this.emit(t)},onEnter(){this.onBlur(),this.$nextTick((()=>this.$emit("submit")))},onInput(t){const e=this.parse(),n=this.pattern.format(e);if(!t||n==t)return this.commit(e),this.emit(e)},onTab(t){""!=this.$refs.input.value&&(this.onBlur(),this.$nextTick((()=>{const e=this.selection();if(this.$refs.input&&e.start===this.$refs.input.selectionStart&&e.end===this.$refs.input.selectionEnd-1)if(t.shiftKey){if(0===e.index)return;this.selectPrev(e.index)}else{if(e.index===this.pattern.parts.length-1)return;this.selectNext(e.index)}else t.shiftKey?this.selectLast():this.selectFirst();t.preventDefault()})))},parse(){let t=this.$refs.input.value;return t=this.$library.dayjs.interpret(t,this.inputType),this.round(t)},round(t){return(null==t?void 0:t.round(this.rounding.unit,this.rounding.size))||null},select(t){var e;t||(t=this.selection()),null==(e=this.$refs.input)||e.setSelectionRange(t.start,t.end+1)},selectFirst(){this.select(this.pattern.parts[0])},selectLast(){this.select(this.pattern.parts[this.pattern.parts.length-1])},selectNext(t){this.select(this.pattern.parts[t+1])},selectPrev(t){this.select(this.pattern.parts[t-1])},selection(){return this.pattern.at(this.$refs.input.selectionStart,this.$refs.input.selectionEnd)},toDatetime(t){return this.round(this.$library.dayjs.iso(t,this.inputType))},toISO(t){return(null==t?void 0:t.toISO(this.inputType))||null}},validations(){return{value:{min:!this.dt||!this.min||(()=>this.dt.validate(this.min,"min",this.rounding.unit)),max:!this.dt||!this.max||(()=>this.dt.validate(this.max,"max",this.rounding.unit)),required:!this.required||(()=>!!this.dt)}}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("input",{directives:[{name:"model",rawName:"v-model",value:t.formatted,expression:"formatted"},{name:"direction",rawName:"v-direction"}],ref:"input",class:"k-text-input k-"+t.type+"-input",attrs:{id:t.id,autofocus:t.autofocus,disabled:t.disabled,placeholder:t.display,required:t.required,autocomplete:"off",spellcheck:"false",type:"text"},domProps:{value:t.formatted},on:{blur:t.onBlur,focus:function(e){return t.$emit("focus")},input:[function(e){e.target.composing||(t.formatted=e.target.value)},function(e){return t.onInput(e.target.value)}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.stopPropagation(),e.preventDefault(),t.onArrowDown.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.stopPropagation(),e.preventDefault(),t.onArrowUp.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.stopPropagation(),e.preventDefault(),t.onEnter.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:t.onTab.apply(null,arguments)}]}})}),[],!1,mi,null,null,null);function mi(t){for(let e in hi)this[e]=hi[e]}var gi=function(){return fi.exports}();const ki={mixins:[vn,yn,_n,Sn,Cn],props:{autocomplete:{type:[Boolean,String],default:"off"},maxlength:Number,minlength:Number,pattern:String,placeholder:String,preselect:Boolean,spellcheck:{type:[Boolean,String],default:"off"},type:{type:String,default:"text"},value:String}},vi={};var bi=Rt({mixins:[ki],inheritAttrs:!1,data(){return{listeners:l(a({},this.$listeners),{input:t=>this.onInput(t.target.value)})}},watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{focus(){this.$refs.input.focus()},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.$refs.input.select()}},validations(){return{value:{required:!this.required||K.required,minLength:!this.minlength||K.minLength(this.minlength),maxLength:!this.maxlength||K.maxLength(this.maxlength),email:"email"!==this.type||K.email,url:"url"!==this.type||K.url,pattern:!this.pattern||(t=>!this.required&&!t||!this.$refs.input.validity.patternMismatch)}}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("input",t._g(t._b({directives:[{name:"direction",rawName:"v-direction"}],ref:"input",staticClass:"k-text-input"},"input",{autocomplete:t.autocomplete,autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,pattern:t.pattern,placeholder:t.placeholder,required:t.required,spellcheck:t.spellcheck,type:t.type,value:t.value},!1),t.listeners))}),[],!1,yi,null,null,null);function yi(t){for(let e in vi)this[e]=vi[e]}var $i=function(){return bi.exports}();const _i={mixins:[ki],props:{autocomplete:{type:String,default:"email"},placeholder:{type:String,default:()=>window.panel.$t("email.placeholder")},type:{type:String,default:"email"}}};const wi={};var xi=Rt({extends:$i,mixins:[_i]},undefined,undefined,!1,Si,null,null,null);function Si(t){for(let e in wi)this[e]=wi[e]}var Ci=function(){return xi.exports}();class Oi extends fs{get schema(){return{content:"bulletList|orderedList"}}}const Ei={inheritAttrs:!1,props:{autofocus:Boolean,marks:{type:[Array,Boolean],default:!0},value:String},data(){return{list:this.value,html:this.value}},computed:{extensions:()=>[new Oi({inline:!0})]},watch:{value(t){t!==this.html&&(this.list=t,this.html=t)}},methods:{focus(){this.$refs.input.focus()},onInput(t){let e=(new DOMParser).parseFromString(t,"text/html").querySelector("ul, ol");e&&0!==e.textContent.trim().length?(this.list=t,this.html=t.replace(/(

|<\/p>)/gi,""),this.$emit("input",this.html)):this.$emit("input",this.list="")}}},Ti={};var Ai=Rt(Ei,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-writer",t._b({ref:"input",staticClass:"k-list-input",attrs:{extensions:t.extensions,nodes:["bulletList","orderedList"],value:t.list},on:{input:t.onInput}},"k-writer",t.$props,!1))}),[],!1,Mi,null,null,null);function Mi(t){for(let e in Ti)this[e]=Ti[e]}var Ii=function(){return Ai.exports}();const Li={mixins:[yn,_n,Cn],props:{max:Number,min:Number,layout:String,options:{type:Array,default:()=>[]},search:[Object,Boolean],separator:{type:String,default:","},sort:Boolean,value:{type:Array,required:!0,default:()=>[]}}},ji={};var Di=Rt({mixins:[Li],inheritAttrs:!1,data(){return{state:this.value,q:null,limit:!0,scrollTop:0}},computed:{draggable(){return this.state.length>1&&!this.sort},dragOptions(){return{disabled:!this.draggable,draggable:".k-tag",delay:1}},emptyLabel(){return this.q?this.$t("search.results.none"):this.$t("options.none")},filtered(){var t;return(null==(t=this.q)?void 0:t.length)>=(this.search.min||0)?this.options.filter((t=>this.isFiltered(t))).map((t=>l(a({},t),{display:this.toHighlightedString(t.text),info:this.toHighlightedString(t.value)}))):this.options.map((t=>l(a({},t),{display:t.text,info:t.value})))},more(){return!this.max||this.state.lengththis.options.findIndex((e=>e.value===t.value));return t.sort(((t,n)=>e(t)-e(n)))},visible(){return this.limit?this.filtered.slice(0,this.search.display||this.filtered.length):this.filtered}},watch:{value(t){this.state=t,this.onInvalid()}},mounted(){this.onInvalid(),this.$events.$on("click",this.close),this.$events.$on("keydown.cmd.s",this.close)},destroyed(){this.$events.$off("click",this.close),this.$events.$off("keydown.cmd.s",this.close)},methods:{add(t){!0===this.more&&(this.state.push(t),this.onInput())},blur(){this.close()},close(){!0===this.$refs.dropdown.isOpen&&(this.$refs.dropdown.close(),this.limit=!0)},escape(){this.q?this.q=null:this.close()},focus(){this.$refs.dropdown.open()},index(t){return this.state.findIndex((e=>e.value===t.value))},isFiltered(t){return String(t.text).match(this.regex)||String(t.value).match(this.regex)},isSelected(t){return-1!==this.index(t)},navigate(t){var e,n,s;"prev"===t&&(t="previous"),null==(s=null==(n=null==(e=document.activeElement)?void 0:e[t+"Sibling"])?void 0:n.focus)||s.call(n)},onClose(){!1===this.$refs.dropdown.isOpen&&(document.activeElement===this.$parent.$el&&(this.q=null),this.$parent.$el.focus())},onInput(){this.$emit("input",this.sorted)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOpen(){this.$nextTick((()=>{var t,e;null==(e=null==(t=this.$refs.search)?void 0:t.focus)||e.call(t),this.$refs.dropdown.$el.querySelector(".k-multiselect-options").scrollTop=this.scrollTop}))},remove(t){this.state.splice(this.index(t),1),this.onInput()},select(t){this.scrollTop=this.$refs.dropdown.$el.querySelector(".k-multiselect-options").scrollTop,t={text:t.text,value:t.value},this.isSelected(t)?this.remove(t):this.add(t)},toHighlightedString(t){return(t=this.$helper.string.stripHTML(t)).replace(this.regex,"$1")}},validations(){return{state:{required:!this.required||K.required,minLength:!this.min||K.minLength(this.min),maxLength:!this.max||K.maxLength(this.max)}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-draggable",{staticClass:"k-multiselect-input",attrs:{list:t.state,options:t.dragOptions,"data-layout":t.layout,element:"k-dropdown"},on:{end:t.onInput},nativeOn:{click:function(e){return t.$refs.dropdown.toggle.apply(null,arguments)}},scopedSlots:t._u([{key:"footer",fn:function(){return[n("k-dropdown-content",{ref:"dropdown",on:{open:t.onOpen,close:t.onClose},nativeOn:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:(e.stopPropagation(),t.close.apply(null,arguments))}}},[t.search?n("k-dropdown-item",{staticClass:"k-multiselect-search",attrs:{icon:"search"}},[n("input",{directives:[{name:"model",rawName:"v-model",value:t.q,expression:"q"}],ref:"search",attrs:{placeholder:t.search.min?t.$t("search.min",{min:t.search.min}):t.$t("search")+" …"},domProps:{value:t.q},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:(e.stopPropagation(),t.escape.apply(null,arguments))},input:function(e){e.target.composing||(t.q=e.target.value)}}})]):t._e(),n("div",{staticClass:"k-multiselect-options scroll-y-auto"},[t._l(t.visible,(function(e){return n("k-dropdown-item",{key:e.value,class:{"k-multiselect-option":!0,selected:t.isSelected(e),disabled:!t.more},attrs:{icon:t.isSelected(e)?"check":"circle-outline"},on:{click:function(n){return n.preventDefault(),t.select(e)}},nativeOn:{keydown:[function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"enter",13,n.key,"Enter")?null:(n.preventDefault(),n.stopPropagation(),t.select(e))},function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"space",32,n.key,[" ","Spacebar"])?null:(n.preventDefault(),n.stopPropagation(),t.select(e))}]}},[n("span",{domProps:{innerHTML:t._s(e.display)}}),n("span",{staticClass:"k-multiselect-value",domProps:{innerHTML:t._s(e.info)}})])})),0===t.filtered.length?n("k-dropdown-item",{staticClass:"k-multiselect-option",attrs:{disabled:!0}},[t._v(" "+t._s(t.emptyLabel)+" ")]):t._e()],2),t.visible.lengththis.onInput(t.target.value),blur:this.onBlur})}},watch:{value(t){this.number=t},number:{immediate:!0,handler(){this.onInvalid()}}},mounted(){this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{decimals(){const t=Number(this.step||0);return Math.floor(t)===t?0:-1!==t.toString().indexOf("e")?parseInt(t.toFixed(16).split(".")[1].split("").reverse().join("")).toString().length:t.toString().split(".")[1].length||0},format(t){if(isNaN(t)||""===t)return"";const e=this.decimals();return t=e?parseFloat(t).toFixed(e):Number.isInteger(this.step)?parseInt(t):parseFloat(t)},clean(){this.number=this.format(this.number)},emit(t){t=parseFloat(t),isNaN(t)&&(t=""),t!==this.value&&this.$emit("input",t)},focus(){this.$refs.input.focus()},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput(t){this.number=t,this.emit(t)},onBlur(){this.clean(),this.emit(this.number)},select(){this.$refs.input.select()}},validations(){return{value:{required:!this.required||K.required,min:!this.min||K.minValue(this.min),max:!this.max||K.maxValue(this.max)}}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("input",t._g(t._b({ref:"input",staticClass:"k-number-input",attrs:{step:t.stepNumber,type:"number"},domProps:{value:t.number},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.ctrlKey?t.clean.apply(null,arguments):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.metaKey?t.clean.apply(null,arguments):null}]}},"input",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,max:t.max,min:t.min,name:t.name,placeholder:t.placeholder,required:t.required},!1),t.listeners))}),[],!1,Fi,null,null,null);function Fi(t){for(let e in qi)this[e]=qi[e]}var zi=function(){return Ri.exports}();const Yi={mixins:[ki],props:{autocomplete:{type:String,default:"new-password"},type:{type:String,default:"password"}}};const Hi={};var Ui=Rt({extends:$i,mixins:[Yi]},undefined,undefined,!1,Ki,null,null,null);function Ki(t){for(let e in Hi)this[e]=Hi[e]}var Ji=function(){return Ui.exports}();const Gi={mixins:[vn,yn,_n,Cn],props:{columns:Number,options:Array,value:[String,Number,Boolean]}},Vi={};var Wi=Rt({mixins:[Gi],inheritAttrs:!1,watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$el.querySelector("input").focus()},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()}},validations(){return{value:{required:!this.required||K.required}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ul",{staticClass:"k-radio-input",style:"--columns:"+t.columns},t._l(t.options,(function(e,s){return n("li",{key:s},[n("input",{staticClass:"k-radio-input-native",attrs:{id:t.id+"-"+s,name:t.id,type:"radio"},domProps:{value:e.value,checked:t.value===e.value},on:{change:function(n){return t.onInput(e.value)}}}),e.info?n("label",{attrs:{for:t.id+"-"+s}},[n("span",{staticClass:"k-radio-input-text",domProps:{innerHTML:t._s(e.text)}}),n("span",{staticClass:"k-radio-input-info",domProps:{innerHTML:t._s(e.info)}})]):n("label",{attrs:{for:t.id+"-"+s},domProps:{innerHTML:t._s(e.text)}}),e.icon?n("k-icon",{attrs:{type:e.icon}}):t._e()],1)})),0)}),[],!1,Xi,null,null,null);function Xi(t){for(let e in Vi)this[e]=Vi[e]}var Zi=function(){return Wi.exports}();const Qi={mixins:[vn,yn,_n,Sn,Cn],props:{default:[Number,String],max:{type:Number,default:100},min:{type:Number,default:0},step:{type:Number,default:1},tooltip:{type:[Boolean,Object],default:()=>({before:null,after:null})},value:[Number,String]}},to={};var eo=Rt({mixins:[Qi],inheritAttrs:!1,data(){return{listeners:l(a({},this.$listeners),{input:t=>this.onInput(t.target.value)})}},computed:{baseline(){return this.min<0?0:this.min},label(){return this.required||this.value||0===this.value?this.format(this.position):"–"},position(){return this.value||0===this.value?this.value:this.default||this.baseline}},watch:{position(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},format(t){const e=document.lang?document.lang.replace("_","-"):"en",n=this.step.toString().split("."),s=n.length>1?n[1].length:0;return new Intl.NumberFormat(e,{minimumFractionDigits:s}).format(t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput(t){this.$emit("input",t)}},validations(){return{position:{required:!this.required||K.required,min:!this.min||K.minValue(this.min),max:!this.max||K.maxValue(this.max)}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-range-input"},[n("input",t._g(t._b({ref:"input",staticClass:"k-range-input-native",style:"--min: "+t.min+"; --max: "+t.max+"; --value: "+t.position,attrs:{type:"range"},domProps:{value:t.position}},"input",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,max:t.max,min:t.min,name:t.name,required:t.required,step:t.step},!1),t.listeners)),t.tooltip?n("span",{staticClass:"k-range-input-tooltip"},[t.tooltip.before?n("span",{staticClass:"k-range-input-tooltip-before"},[t._v(t._s(t.tooltip.before))]):t._e(),n("span",{staticClass:"k-range-input-tooltip-text"},[t._v(t._s(t.label))]),t.tooltip.after?n("span",{staticClass:"k-range-input-tooltip-after"},[t._v(t._s(t.tooltip.after))]):t._e()]):t._e()])}),[],!1,no,null,null,null);function no(t){for(let e in to)this[e]=to[e]}var so=function(){return eo.exports}();const io={mixins:[vn,yn,_n,Sn,Cn],props:{ariaLabel:String,default:String,empty:{type:[Boolean,String],default:!0},placeholder:String,options:{type:Array,default:()=>[]},value:{type:[String,Number,Boolean],default:""}}},oo={};var ro=Rt({mixins:[io],inheritAttrs:!1,data(){return{selected:this.value,listeners:l(a({},this.$listeners),{click:t=>this.onClick(t),change:t=>this.onInput(t.target.value),input:()=>{}})}},computed:{emptyOption(){return this.placeholder||"—"},hasEmptyOption(){return!1!==this.empty&&!(this.required&&this.default)},label(){const t=this.text(this.selected);return""===this.selected||null===this.selected||null===t?this.emptyOption:t}},watch:{value(t){this.selected=t,this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},onClick(t){t.stopPropagation(),this.$emit("click",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput(t){this.selected=t,this.$emit("input",this.selected)},select(){this.focus()},text(t){let e=null;return this.options.forEach((n=>{n.value==t&&(e=n.text)})),e}},validations(){return{selected:{required:!this.required||K.required}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-select-input",attrs:{"data-disabled":t.disabled,"data-empty":""===t.selected}},[n("select",t._g({ref:"input",staticClass:"k-select-input-native",attrs:{id:t.id,autofocus:t.autofocus,"aria-label":t.ariaLabel,disabled:t.disabled,name:t.name,required:t.required},domProps:{value:t.selected}},t.listeners),[t.hasEmptyOption?n("option",{attrs:{disabled:t.required,value:""}},[t._v(" "+t._s(t.emptyOption)+" ")]):t._e(),t._l(t.options,(function(e){return n("option",{key:e.value,attrs:{disabled:e.disabled},domProps:{value:e.value}},[t._v(" "+t._s(e.text)+" ")])}))],2),t._v(" "+t._s(t.label)+" ")])}),[],!1,ao,null,null,null);function ao(t){for(let e in oo)this[e]=oo[e]}var lo=function(){return ro.exports}();const uo={mixins:[ki],props:{allow:{type:String,default:""},formData:{type:Object,default:()=>({})},sync:{type:String}}},co={};var po=Rt({extends:$i,mixins:[uo],data(){return{slug:this.sluggify(this.value),slugs:this.$language?this.$language.rules:this.$system.slugs,syncValue:null}},watch:{formData:{handler(t){return!this.disabled&&(!(!this.sync||void 0===t[this.sync])&&(t[this.sync]!=this.syncValue&&(this.syncValue=t[this.sync],void this.onInput(this.sluggify(this.syncValue)))))},deep:!0,immediate:!0},value(t){(t=this.sluggify(t))!==this.slug&&(this.slug=t,this.$emit("input",this.slug))}},methods:{sluggify(t){return this.$helper.slug(t,[this.slugs,this.$system.ascii],this.allow)},onInput(t){this.slug=this.sluggify(t),this.$emit("input",this.slug)}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("input",t._g(t._b({directives:[{name:"direction",rawName:"v-direction"}],ref:"input",staticClass:"k-text-input",attrs:{autocomplete:"off",spellcheck:"false",type:"text"},domProps:{value:t.slug}},"input",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,pattern:t.pattern,placeholder:t.placeholder,required:t.required},!1),t.listeners))}),[],!1,ho,null,null,null);function ho(t){for(let e in co)this[e]=co[e]}var fo=function(){return po.exports}();const mo={mixins:[vn,yn,_n,Sn,Cn],props:{accept:{type:String,default:"all"},icon:{type:[String,Boolean],default:"tag"},layout:String,max:Number,min:Number,options:{type:Array,default:()=>[]},separator:{type:String,default:","},value:{type:Array,default:()=>[]}}},go={};var ko=Rt({mixins:[mo],inheritAttrs:!1,data(){return{tags:this.prepareTags(this.value),selected:null,newTag:null,tagOptions:this.options.map((t=>{var e;return(null==(e=this.icon)?void 0:e.length)>0&&(t.icon=this.icon),t}),this)}},computed:{dragOptions(){return{delay:1,disabled:!this.draggable,draggable:".k-tag"}},draggable(){return this.tags.length>1},skip(){return this.tags.map((t=>t.value))}},watch:{value(t){this.tags=this.prepareTags(t),this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{addString(t){if(t)if((t=t.trim()).includes(this.separator))t.split(this.separator).forEach((t=>{this.addString(t)}));else if(0!==t.length)if("options"===this.accept){const e=this.options.filter((e=>e.text===t))[0];if(!e)return;this.addTag(e)}else this.addTag({text:t,value:t})},addTag(t){this.addTagToIndex(t),this.$refs.autocomplete.close(),this.$refs.input.focus()},addTagToIndex(t){if("options"===this.accept){if(!this.options.filter((e=>e.value===t.value))[0])return}-1===this.index(t)&&(!this.max||this.tags.length=this.tags.length)return;break;case"first":e=0;break;case"last":e=this.tags.length-1;break;default:e=t}let s=this.tags[e];if(s){let t=this.$refs[s.value];if(null==t?void 0:t[0])return{ref:t[0],tag:s,index:e}}return!1},index(t){return this.tags.findIndex((e=>e.value===t.value))},onInput(){this.$emit("input",this.tags)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},leaveInput(t){0===t.target.selectionStart&&t.target.selectionStart===t.target.selectionEnd&&0!==this.tags.length&&(this.$refs.autocomplete.close(),this.navigate("last"),t.preventDefault())},navigate(t){var e=this.get(t);e?(e.ref.focus(),this.selectTag(e.tag)):"next"===t&&(this.$refs.input.focus(),this.selectTag(null))},prepareTags:t=>!1===Array.isArray(t)?[]:t.map((t=>"string"==typeof t?{text:t,value:t}:t)),remove(t){const e=this.get("prev"),n=this.get("next");this.tags.splice(this.index(t),1),this.onInput(),e?(this.selectTag(e.tag),e.ref.focus()):n?this.selectTag(n.tag):(this.selectTag(null),this.$refs.input.focus())},select(){this.focus()},selectTag(t){this.selected=t},tab(t){var e;(null==(e=this.newTag)?void 0:e.length)>0&&(t.preventDefault(),this.addString(this.newTag))},type(t){this.newTag=t,this.$refs.autocomplete.search(t)}},validations(){return{tags:{required:!this.required||K.required,minLength:!this.min||K.minLength(this.min),maxLength:!this.max||K.maxLength(this.max)}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-draggable",{directives:[{name:"direction",rawName:"v-direction"}],ref:"box",staticClass:"k-tags-input",attrs:{list:t.tags,"data-layout":t.layout,options:t.dragOptions},on:{end:t.onInput},scopedSlots:t._u([{key:"footer",fn:function(){return[n("span",{staticClass:"k-tags-input-element"},[n("k-autocomplete",{ref:"autocomplete",attrs:{html:!0,options:t.options,skip:t.skip},on:{select:t.addTag,leave:function(e){return t.$refs.input.focus()}}},[n("input",{directives:[{name:"model",rawName:"v-model.trim",value:t.newTag,expression:"newTag",modifiers:{trim:!0}}],ref:"input",attrs:{id:t.id,autofocus:t.autofocus,disabled:t.disabled||t.max&&t.tags.length>=t.max,name:t.name,autocomplete:"off",type:"text"},domProps:{value:t.newTag},on:{input:[function(e){e.target.composing||(t.newTag=e.target.value.trim())},function(e){return t.type(e.target.value)}],blur:[t.blurInput,function(e){return t.$forceUpdate()}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.metaKey?t.blurInput.apply(null,arguments):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])||"button"in e&&0!==e.button||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.enter.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.tab.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput.apply(null,arguments)}]}})])],1)]},proxy:!0}])},t._l(t.tags,(function(e,s){return n("k-tag",{key:s,ref:e.value,refInFor:!0,attrs:{removable:!t.disabled,name:"tag"},on:{remove:function(n){return t.remove(e)}},nativeOn:{click:function(t){t.stopPropagation()},blur:function(e){return t.selectTag(null)},focus:function(n){return t.selectTag(e)},keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])||"button"in e&&0!==e.button?null:t.navigate("prev")},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"right",39,e.key,["Right","ArrowRight"])||"button"in e&&2!==e.button?null:t.navigate("next")}],dblclick:function(n){return t.edit(e)}}},[n("span",{domProps:{innerHTML:t._s(e.text)}})])})),1)}),[],!1,vo,null,null,null);function vo(t){for(let e in go)this[e]=go[e]}var bo=function(){return ko.exports}();const yo={mixins:[ki],props:{autocomplete:{type:String,default:"tel"},type:{type:String,default:"tel"}}};const $o={};var _o=Rt({extends:$i,mixins:[yo]},undefined,undefined,!1,wo,null,null,null);function wo(t){for(let e in $o)this[e]=$o[e]}var xo=function(){return _o.exports}();const So={mixins:[vn,yn,_n,Sn,Cn],props:{buttons:{type:[Boolean,Array],default:!0},endpoints:Object,font:String,maxlength:Number,minlength:Number,placeholder:String,preselect:Boolean,size:String,spellcheck:{type:[Boolean,String],default:"off"},theme:String,uploads:[Boolean,Object,Array],value:String}},Co={};var Oo=Rt({mixins:[So],inheritAttrs:!1,data:()=>({over:!1}),watch:{value(){this.onInvalid(),this.$nextTick((()=>{this.resize()}))}},mounted(){this.$nextTick((()=>{this.$library.autosize(this.$refs.input)})),this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{cancel(){this.$refs.input.focus()},dialog(t){if(!this.$refs[t+"Dialog"])throw"Invalid toolbar dialog";this.$refs[t+"Dialog"].open(this.$refs.input,this.selection())},focus(){this.$refs.input.focus()},insert(t){const e=this.$refs.input,n=e.value;setTimeout((()=>{if(e.focus(),document.execCommand("insertText",!1,t),e.value===n){const n=e.value.slice(0,e.selectionStart)+t+e.value.slice(e.selectionEnd);e.value=n,this.$emit("input",n)}})),this.resize()},insertFile(t){(null==t?void 0:t.length)>0&&this.insert(t.map((t=>t.dragText)).join("\n\n"))},insertUpload(t,e){this.insert(e.map((t=>t.dragText)).join("\n\n")),this.$events.$emit("model.update")},onClick(){this.$refs.toolbar&&this.$refs.toolbar.close()},onCommand(t,e){"function"==typeof this[t]?"function"==typeof e?this[t](e(this.$refs.input,this.selection())):this[t](e):window.console.warn(t+" is not a valid command")},onDrop(t){if(this.uploads&&this.$helper.isUploadEvent(t))return this.$refs.fileUpload.drop(t.dataTransfer.files,{url:this.$urls.api+"/"+this.endpoints.field+"/upload",multiple:!1});const e=this.$store.state.drag;"text"===(null==e?void 0:e.type)&&(this.focus(),this.insert(e.data))},onFocus(t){this.$emit("focus",t)},onInput(t){this.$emit("input",t.target.value)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOut(){this.$refs.input.blur(),this.over=!1},onOver(t){if(this.uploads&&this.$helper.isUploadEvent(t))return t.dataTransfer.dropEffect="copy",this.focus(),void(this.over=!0);const e=this.$store.state.drag;"text"===(null==e?void 0:e.type)&&(t.dataTransfer.dropEffect="copy",this.focus(),this.over=!0)},onShortcut(t){!1!==this.buttons&&"Meta"!==t.key&&"Control"!==t.key&&this.$refs.toolbar&&this.$refs.toolbar.shortcut(t.key,t)},onSubmit(t){return this.$emit("submit",t)},prepend(t){this.insert(t+" "+this.selection())},resize(){this.$library.autosize.update(this.$refs.input)},select(){this.$refs.select()},selectFile(){this.$refs.fileDialog.open({endpoint:this.endpoints.field+"/files",multiple:!1})},selection(){const t=this.$refs.input,e=t.selectionStart,n=t.selectionEnd;return t.value.substring(e,n)},uploadFile(){this.$refs.fileUpload.open({url:this.$urls.api+"/"+this.endpoints.field+"/upload",multiple:!1})},wrap(t){this.insert(t+this.selection()+t)}},validations(){return{value:{required:!this.required||K.required,minLength:!this.minlength||K.minLength(this.minlength),maxLength:!this.maxlength||K.maxLength(this.maxlength)}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-textarea-input",attrs:{"data-over":t.over,"data-size":t.size,"data-theme":t.theme}},[n("div",{staticClass:"k-textarea-input-wrapper"},[t.buttons&&!t.disabled?n("k-toolbar",{ref:"toolbar",attrs:{buttons:t.buttons,disabled:t.disabled,uploads:t.uploads},on:{command:t.onCommand},nativeOn:{mousedown:function(t){t.preventDefault()}}}):t._e(),n("textarea",t._b({directives:[{name:"direction",rawName:"v-direction"}],ref:"input",staticClass:"k-textarea-input-native",attrs:{"data-font":t.font},on:{click:t.onClick,focus:t.onFocus,input:t.onInput,keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.metaKey?t.onSubmit.apply(null,arguments):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.ctrlKey?t.onSubmit.apply(null,arguments):null},function(e){return e.metaKey?t.onShortcut.apply(null,arguments):null},function(e){return e.ctrlKey?t.onShortcut.apply(null,arguments):null}],dragover:t.onOver,dragleave:t.onOut,drop:t.onDrop}},"textarea",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,placeholder:t.placeholder,required:t.required,spellcheck:t.spellcheck,value:t.value},!1))],1),n("k-toolbar-email-dialog",{ref:"emailDialog",on:{cancel:t.cancel,submit:function(e){return t.insert(e)}}}),n("k-toolbar-link-dialog",{ref:"linkDialog",on:{cancel:t.cancel,submit:function(e){return t.insert(e)}}}),n("k-files-dialog",{ref:"fileDialog",on:{cancel:t.cancel,submit:function(e){return t.insertFile(e)}}}),t.uploads?n("k-upload",{ref:"fileUpload",on:{success:t.insertUpload}}):t._e()],1)}),[],!1,Eo,null,null,null);function Eo(t){for(let e in Co)this[e]=Co[e]}var To=function(){return Oo.exports}();const Ao={props:{display:{type:String,default:"HH:mm"},max:String,min:String,step:{type:Object,default:()=>({size:5,unit:"minute"})},type:{type:String,default:"time"},value:String}};const Mo={};var Io=Rt({mixins:[gi,Ao],computed:{inputType:()=>"time"}},undefined,undefined,!1,Lo,null,null,null);function Lo(t){for(let e in Mo)this[e]=Mo[e]}var jo=function(){return Io.exports}();const Do={props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],text:{type:[Array,String]},required:Boolean,value:Boolean}},Bo={};var Po=Rt({mixins:[Do],inheritAttrs:!1,computed:{label(){const t=this.text||[this.$t("off"),this.$t("on")];return Array.isArray(t)?this.value?t[1]:t[0]:t}},watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},onEnter(t){"Enter"===t.key&&this.$refs.input.click()},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.$refs.input.focus()}},validations(){return{value:{required:!this.required||K.required}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("label",{staticClass:"k-toggle-input",attrs:{"data-disabled":t.disabled}},[n("input",{ref:"input",staticClass:"k-toggle-input-native",attrs:{id:t.id,disabled:t.disabled,type:"checkbox"},domProps:{checked:t.value},on:{change:function(e){return t.onInput(e.target.checked)}}}),n("span",{staticClass:"k-toggle-input-label",domProps:{innerHTML:t._s(t.label)}})])}),[],!1,No,null,null,null);function No(t){for(let e in Bo)this[e]=Bo[e]}var qo=function(){return Po.exports}();const Ro={mixins:[ki],props:{autocomplete:{type:String,default:"url"},type:{type:String,default:"url"}}};const Fo={};var zo=Rt({extends:$i,mixins:[Ro]},undefined,undefined,!1,Yo,null,null,null);function Yo(t){for(let e in Fo)this[e]=Fo[e]}var Ho=function(){return zo.exports}();const Uo={mixins:[On],inheritAttrs:!1,props:{autofocus:Boolean,empty:String,fieldsets:Object,fieldsetGroups:Object,group:String,max:{type:Number,default:null},value:{type:Array,default:()=>[]}},data:()=>({opened:[]}),computed:{hasFieldsets(){return Object.keys(this.fieldsets).length},isEmpty(){return 0===this.value.length},isFull(){return null!==this.max&&this.value.length>=this.max}},methods:{focus(){this.$refs.blocks.focus()}}},Ko={};var Jo=Rt(Uo,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-blocks-field",scopedSlots:t._u([{key:"options",fn:function(){return[t.hasFieldsets?n("k-dropdown",[n("k-button",{attrs:{icon:"dots"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"add"},on:{click:function(e){return t.$refs.blocks.choose(t.value.length)}}},[t._v(" "+t._s(t.$t("add"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{disabled:t.isEmpty,icon:"template"},on:{click:function(e){return t.$refs.blocks.copyAll()}}},[t._v(" "+t._s(t.$t("copy.all"))+" ")]),n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"download"},on:{click:function(e){return t.$refs.blocks.pasteboard()}}},[t._v(" "+t._s(t.$t("paste"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{disabled:t.isEmpty,icon:"trash"},on:{click:function(e){return t.$refs.blocks.confirmToRemoveAll()}}},[t._v(" "+t._s(t.$t("delete.all"))+" ")])],1)],1):t._e()]},proxy:!0}])},"k-field",t.$props,!1),[n("k-blocks",t._g({ref:"blocks",attrs:{autofocus:t.autofocus,compact:!1,empty:t.empty,endpoints:t.endpoints,fieldsets:t.fieldsets,"fieldset-groups":t.fieldsetGroups,group:t.group,max:t.max,value:t.value},on:{close:function(e){t.opened=e},open:function(e){t.opened=e}}},t.$listeners))],1)}),[],!1,Go,null,null,null);function Go(t){for(let e in Ko)this[e]=Ko[e]}var Vo=function(){return Jo.exports}(),Wo={props:{counter:{type:Boolean,default:!0}},computed:{counterOptions(){var t,e;if(null===this.value||this.disabled||!1===this.counter)return!1;let n=0;return this.value&&(n=Array.isArray(this.value)?this.value.length:String(this.value).length),{count:n,min:null!=(t=this.min)?t:this.minlength,max:null!=(e=this.max)?e:this.maxlength}}}};const Xo={};var Zo=Rt({mixins:[On,Pn,ai,Wo],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-checkboxes-field",attrs:{counter:t.counterOptions}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"checkboxes"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,Qo,null,null,null);function Qo(t){for(let e in Xo)this[e]=Xo[e]}var tr=function(){return Zo.exports}();const er={mixins:[On,Pn,pi],inheritAttrs:!1,props:{calendar:{type:Boolean,default:!0},icon:{type:String,default:"calendar"},time:{type:[Boolean,Object],default:()=>({})},times:{type:Boolean,default:!0}},data(){return{isInvalid:!1,iso:this.toIso(this.value)}},computed:{isEmpty(){return this.time?null===this.iso.date&&this.iso.time:null===this.iso.date}},watch:{value(t,e){t!==e&&(this.iso=this.toIso(t))}},methods:{focus(){this.$refs.dateInput.focus()},now(){const t=this.$library.dayjs();return{date:t.toISO("date"),time:this.time?t.toISO("time"):"00:00:00"}},onInput(){if(this.isEmpty)return this.$emit("input","");const t=this.$library.dayjs.iso(this.iso.date+" "+this.iso.time);(t||null!==this.iso.date&&null!==this.iso.time)&&this.$emit("input",(null==t?void 0:t.toISO())||"")},onCalendarInput(t){var e;null==(e=this.$refs.calendar)||e.close(),this.onDateInput(t)},onDateInput(t){t&&!this.iso.time&&(this.iso.time=this.now().time),this.iso.date=t,this.onInput()},onDateInvalid(t){this.isInvalid=t},onTimeInput(t){t&&!this.iso.date&&(this.iso.date=this.now().date),this.iso.time=t,this.onInput()},onTimesInput(t){var e;null==(e=this.$refs.times)||e.close(),this.onTimeInput(t+":00")},toIso(t){const e=this.$library.dayjs.iso(t);return{date:(null==e?void 0:e.toISO("date"))||null,time:(null==e?void 0:e.toISO("time"))||null}}}},nr={};var sr=Rt(er,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-date-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("div",{ref:"body",staticClass:"k-date-field-body",attrs:{"data-invalid":!t.novalidate&&t.isInvalid,"data-theme":"field"}},[n("k-input",t._b({ref:"dateInput",attrs:{id:t._uid,autofocus:t.autofocus,disabled:t.disabled,display:t.display,max:t.max,min:t.min,required:t.required,value:t.value,theme:"field",type:"date"},on:{invalid:t.onDateInvalid,input:t.onDateInput,submit:function(e){return t.$emit("submit")}},scopedSlots:t._u([t.calendar?{key:"icon",fn:function(){return[n("k-dropdown",[n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,tooltip:t.$t("date.select")},on:{click:function(e){return t.$refs.calendar.toggle()}}}),n("k-dropdown-content",{ref:"calendar",attrs:{align:"right"}},[n("k-calendar",{attrs:{value:t.value,min:t.min,max:t.max},on:{input:t.onCalendarInput}})],1)],1)]},proxy:!0}:null],null,!0)},"k-input",t.$props,!1)),t.time?n("k-input",{ref:"timeInput",attrs:{disabled:t.disabled,display:t.time.display,required:t.required,step:t.time.step,value:t.iso.time,icon:t.time.icon,theme:"field",type:"time"},on:{input:t.onTimeInput,submit:function(e){return t.$emit("submit")}},scopedSlots:t._u([t.times?{key:"icon",fn:function(){return[n("k-dropdown",[n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.time.icon||"clock",tooltip:t.$t("time.select")},on:{click:function(e){return t.$refs.times.toggle()}}}),n("k-dropdown-content",{ref:"times",attrs:{align:"right"}},[n("k-times",{attrs:{display:t.time.display,value:t.value},on:{input:t.onTimesInput}})],1)],1)]},proxy:!0}:null],null,!0)}):t._e()],1)])}),[],!1,ir,null,null,null);function ir(t){for(let e in nr)this[e]=nr[e]}var or=function(){return sr.exports}();const rr={mixins:[On,Pn,_i],inheritAttrs:!1,props:{link:{type:Boolean,default:!0},icon:{type:String,default:"email"}},computed:{mailto(){var t;return(null==(t=this.value)?void 0:t.length)>0?"mailto:"+this.value:null}},methods:{focus(){this.$refs.input.focus()}}},ar={};var lr=Rt(rr,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-email-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"email"},scopedSlots:t._u([{key:"icon",fn:function(){return[t.link?n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,link:t.mailto,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"}}):t._e()]},proxy:!0}])},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,ur,null,null,null);function ur(t){for(let e in ar)this[e]=ar[e]}var cr=function(){return lr.exports}(),dr={mixins:[On],inheritAttrs:!1,props:{empty:String,info:String,link:Boolean,layout:{type:String,default:"list"},max:Number,multiple:Boolean,parent:String,search:Boolean,size:String,text:String,value:{type:Array,default:()=>[]}},data(){return{selected:this.value}},computed:{btnIcon(){return!this.multiple&&this.selected.length>0?"refresh":"add"},btnLabel(){return!this.multiple&&this.selected.length>0?this.$t("change"):this.$t("add")},isInvalid(){return!(!this.required||0!==this.selected.length)||(!!(this.min&&this.selected.lengththis.max))},items(){return this.models.map(this.item)},more(){return!this.max||this.max>this.selected.length}},watch:{value(t){this.selected=t}},methods:{focus(){},item:t=>t,onInput(){this.$emit("input",this.selected)},open(){if(this.disabled)return!1;this.$refs.selector.open({endpoint:this.endpoints.field,max:this.max,multiple:this.multiple,search:this.search,selected:this.selected.map((t=>t.id))})},remove(t){this.selected.splice(t,1),this.onInput()},removeById(t){this.selected=this.selected.filter((e=>e.id!==t)),this.onInput()},select(t){0!==t.length?(this.selected=this.selected.filter((e=>t.filter((t=>t.id===e.id)).length>0)),t.forEach((t=>{0===this.selected.filter((e=>t.id===e.id)).length&&this.selected.push(t)})),this.onInput()):this.selected=[]}}};const pr={mixins:[dr],props:{uploads:[Boolean,Object,Array]},computed:{moreUpload(){return this.more&&this.uploads},options(){return this.uploads?{icon:this.btnIcon,text:this.btnLabel,options:[{icon:"check",text:this.$t("select"),click:"open"},{icon:"upload",text:this.$t("upload"),click:"upload"}]}:{options:[{icon:"check",text:this.$t("select"),click:()=>this.open()}]}},uploadParams(){return{accept:this.uploads.accept,max:this.max,multiple:this.multiple,url:this.$urls.api+"/"+this.endpoints.field+"/upload"}}},created(){this.$events.$on("file.delete",this.removeById)},destroyed(){this.$events.$off("file.delete",this.removeById)},methods:{drop(t){return!1!==this.uploads&&this.$refs.fileUpload.drop(t,this.uploadParams)},prompt(t){if(t.stopPropagation(),this.disabled)return!1;this.moreUpload?this.$refs.options.toggle():this.open()},onAction(t){if(this.moreUpload)switch(t){case"open":return this.open();case"upload":return this.$refs.fileUpload.open(this.uploadParams)}},isSelected(t){return this.selected.find((e=>e.id===t.id))},upload(t,e){!1===this.multiple&&(this.selected=[]),e.forEach((t=>{this.isSelected(t)||this.selected.push(t)})),this.onInput(),this.$events.$emit("model.update")}}},hr={};var fr=Rt(pr,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-files-field",scopedSlots:t._u([t.more&&!t.disabled?{key:"options",fn:function(){return[n("k-button-group",{staticClass:"k-field-options"},[n("k-options-dropdown",t._b({ref:"options",on:{action:t.onAction}},"k-options-dropdown",t.options,!1))],1)]},proxy:!0}:null],null,!0)},"k-field",t.$props,!1),[n("k-dropzone",{attrs:{disabled:!1===t.more},on:{drop:t.drop}},[t.selected.length?[n("k-items",{attrs:{items:t.selected,layout:t.layout,link:t.link,size:t.size,sortable:!t.disabled&&t.selected.length>1},on:{sort:t.onInput,sortChange:function(e){return t.$emit("change",e)}},scopedSlots:t._u([{key:"options",fn:function(e){var s=e.index;return[t.disabled?t._e():n("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(s)}}})]}}],null,!1,1805525116)})]:n("k-empty",{attrs:{layout:t.layout,"data-invalid":t.isInvalid,icon:"image"},on:{click:t.prompt}},[t._v(" "+t._s(t.empty||t.$t("field.files.empty"))+" ")])],2),n("k-files-dialog",{ref:"selector",on:{submit:t.select}}),n("k-upload",{ref:"fileUpload",on:{success:t.upload}})],1)}),[],!1,mr,null,null,null);function mr(t){for(let e in hr)this[e]=hr[e]}var gr=function(){return fr.exports}();const kr={};var vr=Rt({},(function(){var t=this.$createElement;return(this._self._c||t)("div",{staticClass:"k-field k-gap-field"})}),[],!1,br,null,null,null);function br(t){for(let e in kr)this[e]=kr[e]}var yr=function(){return vr.exports}();const $r={mixins:[$n,xn],props:{numbered:Boolean}},_r={};var wr=Rt($r,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-headline-field"},[n("k-headline",{attrs:{"data-numbered":t.numbered,size:"large"}},[t._v(" "+t._s(t.label)+" ")]),t.help?n("footer",{staticClass:"k-field-footer"},[t.help?n("k-text",{staticClass:"k-field-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()],1):t._e()],1)}),[],!1,xr,null,null,null);function xr(t){for(let e in _r)this[e]=_r[e]}var Sr=function(){return wr.exports}();const Cr={};var Or=Rt({mixins:[$n,xn],props:{text:String,theme:{type:String,default:"info"}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-field k-info-field"},[n("k-headline",[t._v(t._s(t.label))]),n("k-box",{attrs:{theme:t.theme}},[n("k-text",{domProps:{innerHTML:t._s(t.text)}})],1),t.help?n("footer",{staticClass:"k-field-footer"},[t.help?n("k-text",{staticClass:"k-field-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()],1):t._e()],1)}),[],!1,Er,null,null,null);function Er(t){for(let e in Cr)this[e]=Cr[e]}var Tr=function(){return Or.exports}();const Ar={props:{blocks:Array,endpoints:Object,fieldsetGroups:Object,fieldsets:Object,id:String,isSelected:Boolean,width:String}},Mr={};var Ir=Rt(Ar,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-column k-layout-column",attrs:{id:t.id,"data-width":t.width,tabindex:"0"},on:{dblclick:function(e){return t.$refs.blocks.choose(t.blocks.length)}}},[n("k-blocks",{ref:"blocks",attrs:{endpoints:t.endpoints,"fieldset-groups":t.fieldsetGroups,fieldsets:t.fieldsets,value:t.blocks,group:"layout"},on:{input:function(e){return t.$emit("input",e)}},nativeOn:{dblclick:function(t){t.stopPropagation()}}})],1)}),[],!1,Lr,null,null,null);function Lr(t){for(let e in Mr)this[e]=Mr[e]}const jr={components:{"k-layout-column":function(){return Ir.exports}()},props:{attrs:[Array,Object],columns:Array,disabled:Boolean,endpoints:Object,fieldsetGroups:Object,fieldsets:Object,id:String,isSelected:Boolean,settings:Object},computed:{tabs(){let t=this.settings.tabs;return Object.entries(t).forEach((([e,n])=>{Object.entries(n.fields).forEach((([n])=>{t[e].fields[n].endpoints={field:this.endpoints.field+"/fields/"+n,section:this.endpoints.section,model:this.endpoints.model}}))})),t}}},Dr={};var Br=Rt(jr,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("section",{staticClass:"k-layout",attrs:{"data-selected":t.isSelected,tabindex:"0"},on:{click:function(e){return t.$emit("select")}}},[n("k-grid",{staticClass:"k-layout-columns"},t._l(t.columns,(function(e,s){return n("k-layout-column",t._b({key:e.id,attrs:{endpoints:t.endpoints,"fieldset-groups":t.fieldsetGroups,fieldsets:t.fieldsets},on:{input:function(n){return t.$emit("updateColumn",{column:e,columnIndex:s,blocks:n})}}},"k-layout-column",e,!1))})),1),t.disabled?t._e():n("nav",{staticClass:"k-layout-toolbar"},[t.settings?n("k-button",{staticClass:"k-layout-toolbar-button",attrs:{tooltip:t.$t("settings"),icon:"settings"},on:{click:function(e){return t.$refs.drawer.open()}}}):t._e(),n("k-dropdown",[n("k-button",{staticClass:"k-layout-toolbar-button",attrs:{icon:"angle-down"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"angle-up"},on:{click:function(e){return t.$emit("prepend")}}},[t._v(" "+t._s(t.$t("insert.before"))+" ")]),n("k-dropdown-item",{attrs:{icon:"angle-down"},on:{click:function(e){return t.$emit("append")}}},[t._v(" "+t._s(t.$t("insert.after"))+" ")]),n("hr"),t.settings?n("k-dropdown-item",{attrs:{icon:"settings"},on:{click:function(e){return t.$refs.drawer.open()}}},[t._v(" "+t._s(t.$t("settings"))+" ")]):t._e(),n("k-dropdown-item",{attrs:{icon:"copy"},on:{click:function(e){return t.$emit("duplicate")}}},[t._v(" "+t._s(t.$t("duplicate"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){return t.$refs.confirmRemoveDialog.open()}}},[t._v(" "+t._s(t.$t("field.layout.delete"))+" ")])],1)],1),n("k-sort-handle")],1),t.settings?n("k-form-drawer",{ref:"drawer",staticClass:"k-layout-drawer",attrs:{tabs:t.tabs,title:t.$t("settings"),value:t.attrs,icon:"settings"},on:{input:function(e){return t.$emit("updateAttrs",e)}}}):t._e(),n("k-remove-dialog",{ref:"confirmRemoveDialog",attrs:{text:t.$t("field.layout.delete.confirm")},on:{submit:function(e){return t.$emit("remove")}}})],1)}),[],!1,Pr,null,null,null);function Pr(t){for(let e in Dr)this[e]=Dr[e]}var Nr=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[t.rows.length?[n("k-draggable",t._b({staticClass:"k-layouts",on:{sort:t.save}},"k-draggable",t.draggableOptions,!1),t._l(t.rows,(function(e,s){return n("k-layout",t._b({key:e.id,attrs:{disabled:t.disabled,endpoints:t.endpoints,"fieldset-groups":t.fieldsetGroups,fieldsets:t.fieldsets,"is-selected":t.selected===e.id,settings:t.settings},on:{append:function(e){return t.selectLayout(s+1)},duplicate:function(n){return t.duplicateLayout(s,e)},prepend:function(e){return t.selectLayout(s)},remove:function(n){return t.removeLayout(e)},select:function(n){t.selected=e.id},updateAttrs:function(e){return t.updateAttrs(s,e)},updateColumn:function(n){return t.updateColumn(Object.assign({},{layout:e,layoutIndex:s},n))}}},"k-layout",e,!1))})),1),t.disabled?t._e():n("k-button",{staticClass:"k-layout-add-button",attrs:{icon:"add"},on:{click:function(e){return t.selectLayout(t.rows.length)}}})]:[n("k-empty",{staticClass:"k-layout-empty",attrs:{icon:"dashboard"},on:{click:function(e){return t.selectLayout(0)}}},[t._v(" "+t._s(t.empty||t.$t("field.layout.empty"))+" ")])],n("k-dialog",{ref:"selector",staticClass:"k-layout-selector",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"}},[n("k-headline",[t._v(t._s(t.$t("field.layout.select")))]),n("ul",t._l(t.layouts,(function(e,s){return n("li",{key:s,staticClass:"k-layout-selector-option"},[n("k-grid",{nativeOn:{click:function(n){return t.addLayout(e)}}},t._l(e,(function(t,e){return n("k-column",{key:e,attrs:{width:t}})})),1)],1)})),0)],1)],2)};const qr={components:{"k-layout":function(){return Br.exports}()},props:{disabled:Boolean,empty:String,endpoints:Object,fieldsetGroups:Object,fieldsets:Object,layouts:Array,max:Number,settings:Object,value:Array},data(){return{currentLayout:null,nextIndex:null,rows:this.value,selected:null}},computed:{draggableOptions(){return{id:this._uid,handle:!0,list:this.rows}}},watch:{value(){this.rows=this.value}},methods:{async addLayout(t){let e=await this.$api.post(this.endpoints.field+"/layout",{columns:t});this.rows.splice(this.nextIndex,0,e),this.layouts.length>1&&this.$refs.selector.close(),this.save()},duplicateLayout(t,e){let n=l(a({},this.$helper.clone(e)),{id:this.$helper.uuid()});n.columns=n.columns.map((t=>(t.id=this.$helper.uuid(),t.blocks=t.blocks.map((t=>(t.id=this.$helper.uuid(),t))),t))),this.rows.splice(t+1,0,n),this.save()},removeLayout(t){const e=this.rows.findIndex((e=>e.id===t.id));-1!==e&&this.$delete(this.rows,e),this.save()},save(){this.$emit("input",this.rows)},selectLayout(t){this.nextIndex=t,1!==this.layouts.length?this.$refs.selector.open():this.addLayout(this.layouts[0])},updateColumn(t){this.rows[t.layoutIndex].columns[t.columnIndex].blocks=t.blocks,this.save()},updateAttrs(t,e){this.rows[t].attrs=e,this.save()}}},Rr={};var Fr=Rt(qr,Nr,[],!1,zr,null,null,null);function zr(t){for(let e in Rr)this[e]=Rr[e]}const Yr={};var Hr=Rt({components:{"k-block-layouts":function(){return Fr.exports}()},mixins:[On],inheritAttrs:!1,props:{empty:String,fieldsetGroups:Object,fieldsets:Object,layouts:{type:Array,default:()=>[["1/1"]]},settings:Object,value:{type:Array,default:()=>[]}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-layout-field"},"k-field",t.$props,!1),[n("k-block-layouts",t._b({on:{input:function(e){return t.$emit("input",e)}}},"k-block-layouts",t.$props,!1))],1)}),[],!1,Ur,null,null,null);function Ur(t){for(let e in Yr)this[e]=Yr[e]}var Kr=function(){return Hr.exports}();const Jr={};var Gr=Rt({},(function(){var t=this.$createElement;return(this._self._c||t)("hr",{staticClass:"k-line-field"})}),[],!1,Vr,null,null,null);function Vr(t){for(let e in Jr)this[e]=Jr[e]}var Wr=function(){return Gr.exports}();const Xr={mixins:[On,Pn],inheritAttrs:!1,props:{marks:[Array,Boolean],value:String},methods:{focus(){this.$refs.input.focus()}}},Zr={};var Qr=Rt(Xr,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-list-field",attrs:{input:t._uid,counter:!1}},"k-field",t.$props,!1),[n("k-input",t._b({ref:"input",attrs:{id:t._uid,marks:t.marks,value:t.value,type:"list",theme:"field"},on:{input:function(e){return t.$emit("input",e)}}},"k-input",t.$props,!1))],1)}),[],!1,ta,null,null,null);function ta(t){for(let e in Zr)this[e]=Zr[e]}var ea=function(){return Qr.exports}();const na={};var sa=Rt({mixins:[On,Pn,Li,Wo],inheritAttrs:!1,props:{icon:{type:String,default:"angle-down"}},mounted(){this.$refs.input.$el.setAttribute("tabindex",0)},methods:{blur(t){this.$refs.input.blur(t)},focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-multiselect-field",attrs:{input:t._uid,counter:t.counterOptions},on:{blur:t.blur},nativeOn:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.preventDefault(),t.focus.apply(null,arguments))}}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"multiselect"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,ia,null,null,null);function ia(t){for(let e in na)this[e]=na[e]}var oa=function(){return sa.exports}();const ra={};var aa=Rt({mixins:[On,Pn,Ni],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-number-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"number"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,la,null,null,null);function la(t){for(let e in ra)this[e]=ra[e]}var ua=function(){return aa.exports}();const ca={};var da=Rt({mixins:[dr]},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-pages-field",scopedSlots:t._u([{key:"options",fn:function(){return[n("k-button-group",{staticClass:"k-field-options"},[t.more&&!t.disabled?n("k-button",{staticClass:"k-field-options-button",attrs:{icon:t.btnIcon,text:t.btnLabel},on:{click:t.open}}):t._e()],1)]},proxy:!0}])},"k-field",t.$props,!1),[t.selected.length?[n("k-items",{attrs:{items:t.selected,layout:t.layout,link:t.link,size:t.size,sortable:!t.disabled&&t.selected.length>1},on:{sort:t.onInput,sortChange:function(e){return t.$emit("change",e)}},scopedSlots:t._u([{key:"options",fn:function(e){var s=e.index;return[t.disabled?t._e():n("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(s)}}})]}}],null,!1,1805525116)})]:n("k-empty",{attrs:{layout:t.layout,"data-invalid":t.isInvalid,icon:"page"},on:{click:t.open}},[t._v(" "+t._s(t.empty||t.$t("field.pages.empty"))+" ")]),n("k-pages-dialog",{ref:"selector",on:{submit:t.select}})],2)}),[],!1,pa,null,null,null);function pa(t){for(let e in ca)this[e]=ca[e]}var ha=function(){return da.exports}();const fa={};var ma=Rt({mixins:[On,Pn,Yi,Wo],inheritAttrs:!1,props:{minlength:{type:Number,default:8},icon:{type:String,default:"key"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-password-field",attrs:{input:t._uid,counter:t.counterOptions},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options")]},proxy:!0}],null,!0)},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"password"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,ga,null,null,null);function ga(t){for(let e in fa)this[e]=fa[e]}var ka=function(){return ma.exports}();const va={};var ba=Rt({mixins:[On,Pn,Gi],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-radio-field"},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"radio"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,ya,null,null,null);function ya(t){for(let e in va)this[e]=va[e]}var $a=function(){return ba.exports}();const _a={};var wa=Rt({mixins:[Pn,On,Qi],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-range-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"range"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,xa,null,null,null);function xa(t){for(let e in _a)this[e]=_a[e]}var Sa=function(){return wa.exports}();const Ca={};var Oa=Rt({mixins:[On,Pn,io],inheritAttrs:!1,props:{icon:{type:String,default:"angle-down"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-select-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"select"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,Ea,null,null,null);function Ea(t){for(let e in Ca)this[e]=Ca[e]}var Ta=function(){return Oa.exports}();const Aa={mixins:[On,Pn,uo],inheritAttrs:!1,props:{icon:{type:String,default:"url"},path:{type:String},wizard:{type:[Boolean,Object],default:!1}},data(){return{slug:this.value}},computed:{preview(){return void 0!==this.help?this.help:void 0!==this.path?this.path+this.value:null}},watch:{value(){this.slug=this.value}},methods:{focus(){this.$refs.input.focus()},onWizard(){var t;this.formData[null==(t=this.wizard)?void 0:t.field]&&(this.slug=this.formData[this.wizard.field])}}},Ma={};var Ia=Rt(Aa,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-slug-field",attrs:{input:t._uid,help:t.preview},scopedSlots:t._u([t.wizard&&t.wizard.text?{key:"options",fn:function(){return[n("k-button",{attrs:{text:t.wizard.text,icon:"wand"},on:{click:t.onWizard}})]},proxy:!0}:null],null,!0)},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,value:t.slug,theme:"field",type:"slug"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,La,null,null,null);function La(t){for(let e in Ma)this[e]=Ma[e]}var ja=function(){return Ia.exports}(),Da={mixins:[On],methods:{columnIsEmpty:t=>null==t||""===t||("object"==typeof t&&0===Object.keys(t).length&&t.constructor===Object||void 0!==t.length&&0===t.length),displayText(t,e){switch(t.type){case"user":return e.email;case"date":{const n=this.$library.dayjs(e),s=!0===t.time?"YYYY-MM-DD HH:mm":"YYYY-MM-DD";return n.isValid()?n.format(s):""}case"tags":case"multiselect":return e.map((t=>t.text)).join(", ");case"checkboxes":return e.map((e=>{let n=e;return t.options.forEach((t=>{t.value===e&&(n=t.text)})),n})).join(", ");case"radio":case"select":{const n=t.options.filter((t=>t.value===e))[0];return n?n.text:null}}return"object"==typeof e&&null!==e?"…":null==e?void 0:e.toString()},previewExists(t){return this.$helper.isComponent(`k-${t}-field-preview`)},width(t){return t?this.$helper.ratio(t,"auto",!1):"auto"}}};const Ba={mixins:[Da],inheritAttrs:!1,props:{columns:Object,duplicate:{type:Boolean,default:!0},empty:String,fields:Object,limit:Number,max:Number,min:Number,prepend:{type:Boolean,default:!1},sortable:{type:Boolean,default:!0},sortBy:String,value:{type:Array,default:()=>[]}},data(){return{autofocus:null,items:this.makeItems(this.value),currentIndex:null,currentModel:null,trash:null,page:1}},computed:{dragOptions(){return{disabled:!this.isSortable,fallbackClass:"k-sortable-row-fallback"}},formFields(){let t={};return Object.keys(this.fields).forEach((e=>{let n=this.fields[e];n.section=this.name,n.endpoints={field:this.endpoints.field+"+"+e,section:this.endpoints.section,model:this.endpoints.model},null===this.autofocus&&!0===n.autofocus&&(this.autofocus=e),t[e]=n})),t},more(){return!0!==this.disabled&&!(this.max&&this.items.length>=this.max)},isInvalid(){return!0!==this.disabled&&(!!(this.min&&this.items.lengththis.max))},isSortable(){return!this.sortBy&&(!this.limit&&(!0!==this.disabled&&(!(this.items.length<=1)&&!1!==this.sortable)))},pagination(){let t=0;return this.limit&&(t=(this.page-1)*this.limit),{page:this.page,offset:t,limit:this.limit,total:this.items.length,align:"center",details:!0}},paginatedItems(){return this.limit?this.items.slice(this.pagination.offset,this.pagination.offset+this.limit):this.items}},watch:{value(t){t!=this.items&&(this.items=this.makeItems(t))}},methods:{add(){if(!0===this.disabled)return!1;if(null!==this.currentIndex)return this.escape(),!1;let t={};Object.keys(this.fields).forEach((e=>{const n=this.fields[e];null!==n.default?t[e]=this.$helper.clone(n.default):t[e]=null})),this.currentIndex="new",this.currentModel=t,this.createForm()},addItem(t){!0===this.prepend?this.items.unshift(t):this.items.push(t)},beforePaginate(){return this.save(this.currentModel)},close(){this.currentIndex=null,this.currentModel=null,this.$events.$off("keydown.esc",this.escape),this.$events.$off("keydown.cmd.s",this.submit),this.$store.dispatch("content/enable")},confirmRemove(t){this.close(),this.trash=t+this.pagination.offset,this.$refs.remove.open()},createForm(t){this.$events.$on("keydown.esc",this.escape),this.$events.$on("keydown.cmd.s",this.submit),this.$store.dispatch("content/disable"),this.$nextTick((()=>{var e;null==(e=this.$refs.form)||e.focus(t||this.autofocus)}))},duplicateItem(t){this.addItem(this.items[t+this.pagination.offset]),this.onInput()},escape(){if("new"===this.currentIndex){let t=Object.values(this.currentModel),e=!0;if(t.forEach((t=>{!1===this.columnIsEmpty(t)&&(e=!1)})),!0===e)return void this.close()}this.submit()},focus(){var t,e;null==(e=null==(t=this.$refs.add)?void 0:t.focus)||e.call(t)},indexOf(t){return this.limit?(this.page-1)*this.limit+t+1:t+1},isActive(t){return this.currentIndex===t},jump(t,e){this.open(t+this.pagination.offset,e)},makeItems(t){return!1===Array.isArray(t)?[]:this.sort(t)},onInput(){this.$emit("input",this.items)},open(t,e){this.currentIndex=t,this.currentModel=this.$helper.clone(this.items[t]),this.createForm(e)},paginate(t){this.open(t.offset)},paginateItems(t){this.page=t.page},remove(){if(null===this.trash)return!1;this.items.splice(this.trash,1),this.trash=null,this.$refs.remove.close(),this.onInput(),0===this.paginatedItems.length&&this.page>1&&this.page--,this.items=this.sort(this.items)},sort(t){return this.sortBy?t.sortBy(this.sortBy):t},async save(){if(null!==this.currentIndex&&void 0!==this.currentIndex)try{return await this.validate(this.currentModel),"new"===this.currentIndex?this.addItem(this.currentModel):this.items[this.currentIndex]=this.currentModel,this.items=this.sort(this.items),this.onInput(),!0}catch(t){throw this.$store.dispatch("notification/error",{message:this.$t("error.form.incomplete"),details:t}),t}},async submit(){try{await this.save(),this.close()}catch(t){}},async validate(t){const e=await this.$api.post(this.endpoints.field+"/validate",t);if(e.length>0)throw e;return!0},update(t,e,n){this.items[t][e]=n,this.onInput()}}},Pa={};var Na=Rt(Ba,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-structure-field",nativeOn:{click:function(t){t.stopPropagation()}},scopedSlots:t._u([{key:"options",fn:function(){return[t.more&&null===t.currentIndex?n("k-button",{ref:"add",attrs:{id:t._uid,text:t.$t("add"),icon:"add"},on:{click:t.add}}):t._e()]},proxy:!0}])},"k-field",t.$props,!1),[null!==t.currentIndex?[n("div",{staticClass:"k-structure-backdrop",on:{click:t.escape}}),n("section",{staticClass:"k-structure-form"},[n("k-form",{ref:"form",staticClass:"k-structure-form-fields",attrs:{fields:t.formFields},on:{input:t.onInput,submit:t.submit},model:{value:t.currentModel,callback:function(e){t.currentModel=e},expression:"currentModel"}}),n("footer",{staticClass:"k-structure-form-buttons"},[n("k-button",{staticClass:"k-structure-form-cancel-button",attrs:{text:t.$t("cancel"),icon:"cancel"},on:{click:t.close}}),"new"!==t.currentIndex?n("k-pagination",{attrs:{dropdown:!1,total:t.items.length,limit:1,page:t.currentIndex+1,details:!0,validate:t.beforePaginate},on:{paginate:t.paginate}}):t._e(),n("k-button",{staticClass:"k-structure-form-submit-button",attrs:{text:t.$t("new"!==t.currentIndex?"confirm":"add"),icon:"check"},on:{click:t.submit}})],1)],1)]:0===t.items.length?n("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"list-bullet"},on:{click:t.add}},[t._v(" "+t._s(t.empty||t.$t("field.structure.empty"))+" ")]):[n("table",{staticClass:"k-structure-table",attrs:{"data-invalid":t.isInvalid,"data-sortable":t.isSortable}},[n("thead",[n("tr",[n("th",{staticClass:"k-structure-table-index"},[t._v("#")]),t._l(t.columns,(function(e,s){return n("th",{key:s+"-header",staticClass:"k-structure-table-column",style:"width:"+t.width(e.width),attrs:{"data-align":e.align}},[t._v(" "+t._s(e.label)+" ")])})),t.disabled?t._e():n("th")],2)]),n("k-draggable",{directives:[{name:"direction",rawName:"v-direction"}],attrs:{list:t.items,"data-disabled":t.disabled,options:t.dragOptions,handle:!0,element:"tbody"},on:{end:t.onInput}},t._l(t.paginatedItems,(function(e,s){return n("tr",{key:s,on:{click:function(t){t.stopPropagation()}}},[n("td",{staticClass:"k-structure-table-index"},[t.isSortable?n("k-sort-handle"):t._e(),n("span",{staticClass:"k-structure-table-index-number"},[t._v(t._s(t.indexOf(s)))])],1),t._l(t.columns,(function(i,o){return n("td",{key:o,staticClass:"k-structure-table-column",style:"width:"+t.width(i.width),attrs:{title:i.label,"data-align":i.align},on:{click:function(e){return t.jump(s,o)}}},[!1===t.columnIsEmpty(e[o])?[t.previewExists(i.type)?n("k-"+i.type+"-field-preview",{tag:"component",attrs:{value:e[o],column:i,field:t.fields[o]},on:{input:function(e){return t.update(s,o,e)}}}):[n("p",{staticClass:"k-structure-table-text"},[t._v(" "+t._s(i.before)+" "+t._s(t.displayText(t.fields[o],e[o])||"–")+" "+t._s(i.after)+" ")])]]:t._e()],2)})),t.disabled?t._e():n("td",{staticClass:"k-structure-table-options"},[t.duplicate&&t.more&&null===t.currentIndex?[n("k-button",{key:s,ref:"actionsToggle",refInFor:!0,staticClass:"k-structure-table-options-button",attrs:{icon:"dots"},on:{click:function(e){t.$refs[s+"-actions"][0].toggle()}}}),n("k-dropdown-content",{ref:s+"-actions",refInFor:!0,attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"copy"},on:{click:function(e){return t.duplicateItem(s)}}},[t._v(" "+t._s(t.$t("duplicate"))+" ")]),n("k-dropdown-item",{attrs:{icon:"remove"},on:{click:function(e){return t.confirmRemove(s)}}},[t._v(" "+t._s(t.$t("remove"))+" ")])],1)]:[n("k-button",{staticClass:"k-structure-table-options-button",attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.confirmRemove(s)}}})]],2)],2)})),0)],1),t.limit?n("k-pagination",t._b({on:{paginate:t.paginateItems}},"k-pagination",t.pagination,!1)):t._e(),t.disabled?t._e():n("k-dialog",{ref:"remove",attrs:{"submit-button":t.$t("delete"),theme:"negative"},on:{submit:t.remove}},[n("k-text",[t._v(t._s(t.$t("field.structure.delete.confirm")))])],1)]],2)}),[],!1,qa,null,null,null);function qa(t){for(let e in Pa)this[e]=Pa[e]}var Ra=function(){return Na.exports}();const Fa={};var za=Rt({mixins:[On,Pn,mo,Wo],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-tags-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"tags"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,Ya,null,null,null);function Ya(t){for(let e in Fa)this[e]=Fa[e]}var Ha=function(){return za.exports}();const Ua={};var Ka=Rt({mixins:[On,Pn,yo],inheritAttrs:!1,props:{icon:{type:String,default:"phone"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-tel-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"tel"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,Ja,null,null,null);function Ja(t){for(let e in Ua)this[e]=Ua[e]}var Ga=function(){return Ka.exports}();const Va={};var Wa=Rt({mixins:[On,Pn,ki,Wo],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()},select(){this.$refs.input.select()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-text-field",attrs:{input:t._uid,counter:t.counterOptions},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options")]},proxy:!0}],null,!0)},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,Xa,null,null,null);function Xa(t){for(let e in Va)this[e]=Va[e]}var Za=function(){return Wa.exports}();const Qa={};var tl=Rt({mixins:[On,Pn,So,Wo],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-textarea-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,type:"textarea",theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,el,null,null,null);function el(t){for(let e in Qa)this[e]=Qa[e]}var nl=function(){return tl.exports}();const sl={mixins:[On,Pn,Ao],inheritAttrs:!1,props:{icon:{type:String,default:"clock"},times:{type:Boolean,default:!0}},methods:{focus(){this.$refs.input.focus()},select(t){var e;this.$emit("input",t),null==(e=this.$refs.times)||e.close()}}},il={};var ol=Rt(sl,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-time-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"time"},on:{input:function(e){return t.$emit("input",e||"")}},scopedSlots:t._u([t.times?{key:"icon",fn:function(){return[n("k-dropdown",[n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon||"clock",tooltip:t.$t("time.select")},on:{click:function(e){return t.$refs.times.toggle()}}}),n("k-dropdown-content",{ref:"times",attrs:{align:"right"}},[n("k-times",{attrs:{display:t.display,value:t.value},on:{input:t.select}})],1)],1)]},proxy:!0}:null],null,!0)},"k-input",t.$props,!1))],1)}),[],!1,rl,null,null,null);function rl(t){for(let e in il)this[e]=il[e]}var al=function(){return ol.exports}();const ll={};var ul=Rt({mixins:[On,Pn,Do],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-toggle-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"toggle"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,cl,null,null,null);function cl(t){for(let e in ll)this[e]=ll[e]}var dl=function(){return ul.exports}();const pl={mixins:[On,Pn,Ro],inheritAttrs:!1,props:{link:{type:Boolean,default:!0},icon:{type:String,default:"url"}},methods:{focus(){this.$refs.input.focus()}}},hl={};var fl=Rt(pl,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-url-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[n("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"url"},scopedSlots:t._u([{key:"icon",fn:function(){return[t.link?n("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,link:t.value,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"}}):t._e()]},proxy:!0}])},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,ml,null,null,null);function ml(t){for(let e in hl)this[e]=hl[e]}var gl=function(){return fl.exports}();const kl={};var vl=Rt({mixins:[dr]},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-users-field",scopedSlots:t._u([{key:"options",fn:function(){return[n("k-button-group",{staticClass:"k-field-options"},[t.more&&!t.disabled?n("k-button",{staticClass:"k-field-options-button",attrs:{icon:t.btnIcon,text:t.btnLabel},on:{click:t.open}}):t._e()],1)]},proxy:!0}])},"k-field",t.$props,!1),[t.selected.length?[n("k-items",{attrs:{items:t.selected,layout:t.layout,link:t.link,size:t.size,sortable:!t.disabled&&t.selected.length>1},on:{sort:t.onInput,sortChange:function(e){return t.$emit("change",e)}},scopedSlots:t._u([{key:"options",fn:function(e){var s=e.index;return[t.disabled?t._e():n("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(s)}}})]}}],null,!1,1805525116)})]:n("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"users"},on:{click:t.open}},[t._v(" "+t._s(t.empty||t.$t("field.users.empty"))+" ")]),n("k-users-dialog",{ref:"selector",on:{submit:t.select}})],2)}),[],!1,bl,null,null,null);function bl(t){for(let e in kl)this[e]=kl[e]}var yl=function(){return vl.exports}();const $l={};var _l=Rt({mixins:[On,Pn,Js],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-field",t._b({staticClass:"k-writer-field",attrs:{input:t._uid,counter:!1}},"k-field",t.$props,!1),[n("k-input",t._b({attrs:{after:t.after,before:t.before,icon:t.icon,theme:"field"}},"k-input",t.$props,!1),[n("k-writer",t._b({ref:"input",staticClass:"k-writer-field-input",attrs:{value:t.value},on:{input:function(e){return t.$emit("input",e)}}},"k-writer",t.$props,!1))],1)],1)}),[],!1,wl,null,null,null);function wl(t){for(let e in $l)this[e]=$l[e]}var xl=function(){return _l.exports}();const Sl=function(t){this.command("insert",((e,n)=>{let s=[];return n.split("\n").forEach(((e,n)=>{let i="ol"===t?n+1+".":"-";s.push(i+" "+e)})),s.join("\n")}))},Cl={layout:["headlines","bold","italic","|","link","email","file","|","code","ul","ol"],props:{buttons:{type:[Boolean,Array],default:!0},uploads:[Boolean,Object,Array]},data(){let t={},e={},n=[],s=this.commands();return!1===this.buttons?t:(Array.isArray(this.buttons)&&(n=this.buttons),!0!==Array.isArray(this.buttons)&&(n=this.$options.layout),n.forEach(((n,i)=>{if("|"===n)t["divider-"+i]={divider:!0};else if(s[n]){let i=s[n];t[n]=i,i.shortcut&&(e[i.shortcut]=n)}})),{layout:t,shortcuts:e})},methods:{command(t,e){"function"==typeof t?t.apply(this):this.$emit("command",t,e)},close(){Object.keys(this.$refs).forEach((t=>{const e=this.$refs[t][0];"function"==typeof(null==e?void 0:e.close)&&e.close()}))},fileCommandSetup(){let t={label:this.$t("toolbar.button.file"),icon:"attachment"};return!1===this.uploads?t.command="selectFile":t.dropdown={select:{label:this.$t("toolbar.button.file.select"),icon:"check",command:"selectFile"},upload:{label:this.$t("toolbar.button.file.upload"),icon:"upload",command:"uploadFile"}},t},commands(){return{headlines:{label:this.$t("toolbar.button.headings"),icon:"title",dropdown:{h1:{label:this.$t("toolbar.button.heading.1"),icon:"title",command:"prepend",args:"#"},h2:{label:this.$t("toolbar.button.heading.2"),icon:"title",command:"prepend",args:"##"},h3:{label:this.$t("toolbar.button.heading.3"),icon:"title",command:"prepend",args:"###"}}},bold:{label:this.$t("toolbar.button.bold"),icon:"bold",command:"wrap",args:"**",shortcut:"b"},italic:{label:this.$t("toolbar.button.italic"),icon:"italic",command:"wrap",args:"*",shortcut:"i"},link:{label:this.$t("toolbar.button.link"),icon:"url",shortcut:"k",command:"dialog",args:"link"},email:{label:this.$t("toolbar.button.email"),icon:"email",shortcut:"e",command:"dialog",args:"email"},file:this.fileCommandSetup(),code:{label:this.$t("toolbar.button.code"),icon:"code",command:"wrap",args:"`"},ul:{label:this.$t("toolbar.button.ul"),icon:"list-bullet",command(){return Sl.apply(this,["ul"])}},ol:{label:this.$t("toolbar.button.ol"),icon:"list-numbers",command(){return Sl.apply(this,["ol"])}}}},shortcut(t,e){if(this.shortcuts[t]){const n=this.layout[this.shortcuts[t]];if(!n)return!1;e.preventDefault(),this.command(n.command,n.args)}}}},Ol={};var El=Rt(Cl,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-toolbar"},[n("div",{staticClass:"k-toolbar-wrapper"},[n("div",{staticClass:"k-toolbar-buttons"},[t._l(t.layout,(function(e,s){return[e.divider?[n("span",{key:s,staticClass:"k-toolbar-divider"})]:e.dropdown?[n("k-dropdown",{key:s},[n("k-button",{key:s,staticClass:"k-toolbar-button",attrs:{icon:e.icon,tooltip:e.label,tabindex:"-1"},on:{click:function(e){t.$refs[s+"-dropdown"][0].toggle()}}}),n("k-dropdown-content",{ref:s+"-dropdown",refInFor:!0},t._l(e.dropdown,(function(e,s){return n("k-dropdown-item",{key:s,attrs:{icon:e.icon},on:{click:function(n){return t.command(e.command,e.args)}}},[t._v(" "+t._s(e.label)+" ")])})),1)],1)]:[n("k-button",{key:s,staticClass:"k-toolbar-button",attrs:{icon:e.icon,tooltip:e.label,tabindex:"-1"},on:{click:function(n){return t.command(e.command,e.args)}}})]]}))],2)])])}),[],!1,Tl,null,null,null);function Tl(t){for(let e in Ol)this[e]=Ol[e]}var Al=function(){return El.exports}();const Ml={};var Il=Rt({data(){return{value:{email:null,text:null},fields:{email:{label:this.$t("email"),type:"email"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext(){return this.$config.kirbytext}},methods:{open(t,e){this.value.text=e,this.$refs.dialog.open()},cancel(){this.$emit("cancel")},createKirbytext(){var t;const e=this.value.email||"";return(null==(t=this.value.text)?void 0:t.length)>0?`(email: ${e} text: ${this.value.text})`:`(email: ${e})`},createMarkdown(){var t;const e=this.value.email||"";return(null==(t=this.value.text)?void 0:t.length)>0?`[${this.value.text}](mailto:${e})`:`<${e}>`},submit(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={email:null,text:null},this.$refs.dialog.close()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("insert")},on:{close:t.cancel,submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)}),[],!1,Ll,null,null,null);function Ll(t){for(let e in Ml)this[e]=Ml[e]}var jl=function(){return Il.exports}();const Dl={};var Bl=Rt({data(){return{value:{url:null,text:null},fields:{url:{label:this.$t("link"),type:"text",placeholder:this.$t("url.placeholder"),icon:"url"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext(){return this.$config.kirbytext}},methods:{open(t,e){this.value.text=e,this.$refs.dialog.open()},cancel(){this.$emit("cancel")},createKirbytext(){return this.value.text.length>0?`(link: ${this.value.url} text: ${this.value.text})`:`(link: ${this.value.url})`},createMarkdown(){return this.value.text.length>0?`[${this.value.text}](${this.value.url})`:`<${this.value.url}>`},submit(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={url:null,text:null},this.$refs.dialog.close()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("insert")},on:{close:t.cancel,submit:function(e){return t.$refs.form.submit()}}},[n("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)}),[],!1,Pl,null,null,null);function Pl(t){for(let e in Dl)this[e]=Dl[e]}var Nl=function(){return Bl.exports}();const ql={};var Rl=Rt({props:{field:Object,value:String},computed:{text(){var t;const e=this.$library.dayjs.iso(this.value);if(e){let n=this.field.display;return(null==(t=this.field.time)?void 0:t.display)&&(n+=" "+this.field.time.display),e.format(n)}return""}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("p",{staticClass:"k-date-field-preview"},[t._v(" "+t._s(t.text)+" ")])])}),[],!1,Fl,null,null,null);function Fl(t){for(let e in ql)this[e]=ql[e]}var zl=function(){return Rl.exports}();const Yl={};var Hl=Rt({props:{column:{type:Object,default:()=>({})},value:String},computed:{link(){return this.value}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("p",{staticClass:"k-url-field-preview"},[t._v(" "+t._s(t.column.before)+" "),n("k-link",{attrs:{to:t.link,target:"_blank"},nativeOn:{click:function(t){t.stopPropagation()}}},[t._v(" "+t._s(t.value)+" ")]),t._v(" "+t._s(t.column.after)+" ")],1)}),[],!1,Ul,null,null,null);function Ul(t){for(let e in Yl)this[e]=Yl[e]}var Kl=function(){return Hl.exports}();const Jl={};var Gl=Rt({extends:Kl,computed:{link(){var t;return(null==(t=this.value)?void 0:t.length)>0?"mailto:"+this.value:null}}},undefined,undefined,!1,Vl,null,null,null);function Vl(t){for(let e in Jl)this[e]=Jl[e]}var Wl=function(){return Gl.exports}();const Xl={};var Zl=Rt({props:{value:Array}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-files-field-preview"},t._l(t.value,(function(t){return n("li",{key:t.url},[n("k-link",{attrs:{title:t.filename,to:t.link},nativeOn:{click:function(t){t.stopPropagation()}}},[n("k-item-image",{attrs:{image:t.image,layout:"list"}})],1)],1)})),0):t._e()}),[],!1,Ql,null,null,null);function Ql(t){for(let e in Xl)this[e]=Xl[e]}var tu=function(){return Zl.exports}();const eu={};var nu=Rt({inheritAttrs:!1,props:{value:String}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"k-list-field-preview",domProps:{innerHTML:t._s(t.value)}})}),[],!1,su,null,null,null);function su(t){for(let e in eu)this[e]=eu[e]}var iu=function(){return nu.exports}();const ou={};var ru=Rt({props:{value:Array}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-pages-field-preview"},t._l(t.value,(function(e){return n("li",{key:e.id},[n("figure",[n("k-link",{attrs:{title:e.id,to:e.link},nativeOn:{click:function(t){t.stopPropagation()}}},[n("k-item-image",{staticClass:"k-pages-field-preview-image",attrs:{image:e.image,layout:"list"}}),n("figcaption",[t._v(" "+t._s(e.text)+" ")])],1)],1)])})),0):t._e()}),[],!1,au,null,null,null);function au(t){for(let e in ou)this[e]=ou[e]}var lu=function(){return ru.exports}();const uu={};var cu=Rt({props:{field:Object,value:String},computed:{text(){const t=this.$library.dayjs.iso(this.value,"time");return(null==t?void 0:t.format(this.field.display))||""}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",[n("p",{staticClass:"k-time-field-preview"},[t._v(" "+t._s(t.text)+" ")])])}),[],!1,du,null,null,null);function du(t){for(let e in uu)this[e]=uu[e]}var pu=function(){return cu.exports}();const hu={props:{field:Object,value:Boolean,column:Object},computed:{text(){return!1!==this.column.text?this.field.text:null}}},fu={};var mu=Rt(hu,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-input",{staticClass:"k-toggle-field-preview",attrs:{text:t.text,type:"toggle"},on:{input:function(e){return t.$emit("input",e)}},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})}),[],!1,gu,null,null,null);function gu(t){for(let e in fu)this[e]=fu[e]}var ku=function(){return mu.exports}();const vu={};var bu=Rt({props:{value:Array}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.value?n("ul",{staticClass:"k-users-field-preview"},t._l(t.value,(function(e){return n("li",{key:e.email},[n("figure",[n("k-link",{attrs:{title:e.email,to:e.link},nativeOn:{click:function(t){t.stopPropagation()}}},[e.avatar?n("k-image",{staticClass:"k-users-field-preview-avatar",attrs:{src:e.avatar.url,back:"pattern"}}):n("k-icon",{staticClass:"k-users-field-preview-avatar",attrs:{type:"user",back:"pattern"}}),n("figcaption",[t._v(" "+t._s(e.username)+" ")])],1)],1)])})),0):t._e()}),[],!1,yu,null,null,null);function yu(t){for(let e in vu)this[e]=vu[e]}var $u=function(){return bu.exports}();const _u={};var wu=Rt({inheritAttrs:!1,props:{value:String}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"k-writer-field-preview",domProps:{innerHTML:t._s(t.value)}})}),[],!1,xu,null,null,null);function xu(t){for(let e in _u)this[e]=_u[e]}var Su=function(){return wu.exports}();const Cu={props:{cover:Boolean,ratio:String},computed:{ratioPadding(){return this.$helper.ratio(this.ratio)}}},Ou={};var Eu=Rt(Cu,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("span",{staticClass:"k-aspect-ratio",style:{"padding-bottom":t.ratioPadding},attrs:{"data-cover":t.cover}},[t._t("default")],2)}),[],!1,Tu,null,null,null);function Tu(t){for(let e in Ou)this[e]=Ou[e]}var Au=function(){return Eu.exports}();const Mu={};var Iu=Rt({},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-bar"},[t.$slots.left?n("div",{staticClass:"k-bar-slot",attrs:{"data-position":"left"}},[t._t("left")],2):t._e(),t.$slots.center?n("div",{staticClass:"k-bar-slot",attrs:{"data-position":"center"}},[t._t("center")],2):t._e(),t.$slots.right?n("div",{staticClass:"k-bar-slot",attrs:{"data-position":"right"}},[t._t("right")],2):t._e()])}),[],!1,Lu,null,null,null);function Lu(t){for(let e in Mu)this[e]=Mu[e]}var ju=function(){return Iu.exports}();const Du={props:{theme:{type:String,default:"none"},text:String,html:{type:Boolean,default:!1}}},Bu={};var Pu=Rt(Du,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",t._g({staticClass:"k-box",attrs:{"data-theme":t.theme}},t.$listeners),[t._t("default",(function(){return[t.html?n("k-text",{domProps:{innerHTML:t._s(t.text)}}):n("k-text",[t._v(" "+t._s(t.text)+" ")])]}))],2)}),[],!1,Nu,null,null,null);function Nu(t){for(let e in Bu)this[e]=Bu[e]}var qu=function(){return Pu.exports}();const Ru={props:{help:String,items:{type:[Array,Object],default:()=>[]},layout:{type:String,default:"list"},size:String,sortable:Boolean,pagination:{type:[Boolean,Object],default:()=>!1}},computed:{hasPagination(){return!1!==this.pagination&&(!0!==this.paginationOptions.hide&&!(this.pagination.total<=this.pagination.limit))},hasFooter(){return!(!this.hasPagination&&!this.help)},paginationOptions(){const t="object"!=typeof this.pagination?{}:this.pagination;return a({limit:10,details:!0,keys:!1,total:0,hide:!1},t)}},watch:{$props(){this.$forceUpdate()}},methods:{onOption(...t){this.$emit("action",...t),this.$emit("option",...t)}}},Fu={};var zu=Rt(Ru,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-collection"},[n("k-items",{attrs:{items:t.items,layout:t.layout,size:t.size,sortable:t.sortable},on:{option:t.onOption,sort:function(e){return t.$emit("sort",e)},change:function(e){return t.$emit("change",e)}}}),t.hasFooter?n("footer",{staticClass:"k-collection-footer"},[t.help?n("k-text",{staticClass:"k-collection-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e(),n("div",{staticClass:"k-collection-pagination"},[t.hasPagination?n("k-pagination",t._b({on:{paginate:function(e){return t.$emit("paginate",e)}}},"k-pagination",t.paginationOptions,!1)):t._e()],1)],1):t._e()],1)}),[],!1,Yu,null,null,null);function Yu(t){for(let e in Fu)this[e]=Fu[e]}var Hu=function(){return zu.exports}();const Uu={props:{width:String,sticky:Boolean}},Ku={};var Ju=Rt(Uu,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-column",attrs:{"data-width":t.width,"data-sticky":t.sticky}},[n("div",[t._t("default")],2)])}),[],!1,Gu,null,null,null);function Gu(t){for(let e in Ku)this[e]=Ku[e]}var Vu=function(){return Ju.exports}();const Wu={props:{disabled:{type:Boolean,default:!1}},data:()=>({files:[],dragging:!1,over:!1}),methods:{cancel(){this.reset()},reset(){this.dragging=!1,this.over=!1},onDrop(t){return!0===this.disabled||!1===this.$helper.isUploadEvent(t)?this.reset():(this.$events.$emit("dropzone.drop"),this.files=t.dataTransfer.files,this.$emit("drop",this.files),void this.reset())},onEnter(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(this.dragging=!0)},onLeave(){this.reset()},onOver(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(t.dataTransfer.dropEffect="copy",this.over=!0)}}},Xu={};var Zu=Rt(Wu,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"k-dropzone",attrs:{"data-dragging":t.dragging,"data-over":t.over},on:{dragenter:t.onEnter,dragleave:t.onLeave,dragover:t.onOver,drop:t.onDrop}},[t._t("default")],2)}),[],!1,Qu,null,null,null);function Qu(t){for(let e in Xu)this[e]=Xu[e]}var tc=function(){return Zu.exports}();const ec={};var nc=Rt({props:{text:String,icon:String,layout:{type:String,default:"list"}},computed:{element(){return void 0!==this.$listeners.click?"button":"div"}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.element,t._g({tag:"component",staticClass:"k-empty",attrs:{"data-layout":t.layout,type:"button"===t.element&&"button"}},t.$listeners),[t.icon?n("k-icon",{attrs:{type:t.icon}}):t._e(),n("p",[t._t("default")],2)],1)}),[],!1,sc,null,null,null);function sc(t){for(let e in ec)this[e]=ec[e]}var ic=function(){return nc.exports}();const oc={};var rc=Rt({props:{details:Array,image:Object,url:String}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-file-preview"},[n("k-view",{staticClass:"k-file-preview-layout"},[n("div",{staticClass:"k-file-preview-image"},[n("k-link",{staticClass:"k-file-preview-image-link",attrs:{to:t.url,title:t.$t("open"),target:"_blank"}},[n("k-item-image",{attrs:{image:t.image,layout:"cards"}})],1)],1),n("div",{staticClass:"k-file-preview-details"},[n("ul",t._l(t.details,(function(e){return n("li",{key:e.title},[n("h3",[t._v(t._s(e.title))]),n("p",[e.link?n("k-link",{attrs:{to:e.link,tabindex:"-1",target:"_blank"}},[t._v(" /"+t._s(e.text)+" ")]):[t._v(" "+t._s(e.text)+" ")]],2)])})),0)])])],1)}),[],!1,ac,null,null,null);function ac(t){for(let e in oc)this[e]=oc[e]}var lc=function(){return rc.exports}();const uc={};var cc=Rt({props:{gutter:String}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"k-grid",attrs:{"data-gutter":t.gutter}},[t._t("default")],2)}),[],!1,dc,null,null,null);function dc(t){for(let e in uc)this[e]=uc[e]}var pc=function(){return cc.exports}();const hc={props:{editable:Boolean,tab:String,tabs:{type:Array,default:()=>[]}},computed:{tabsWithBadges(){const t=Object.keys(this.$store.getters["content/changes"]());return this.tabs.map((e=>{let n=[];return Object.values(e.columns).forEach((t=>{Object.values(t.sections).forEach((t=>{"fields"===t.type&&Object.keys(t.fields).forEach((t=>{n.push(t)}))}))})),e.badge=n.filter((e=>t.includes(e.toLowerCase()))).length,e}))}}},fc={};var mc=Rt(hc,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("header",{staticClass:"k-header",attrs:{"data-editable":t.editable}},[n("k-headline",{attrs:{tag:"h1",size:"huge"}},[t.editable&&t.$listeners.edit?n("span",{staticClass:"k-headline-editable",on:{click:function(e){return t.$emit("edit")}}},[t._t("default"),n("k-icon",{attrs:{type:"edit"}})],2):t._t("default")],2),t.$slots.left||t.$slots.right?n("k-bar",{staticClass:"k-header-buttons",scopedSlots:t._u([{key:"left",fn:function(){return[t._t("left")]},proxy:!0},{key:"right",fn:function(){return[t._t("right")]},proxy:!0}],null,!0)}):t._e(),n("k-tabs",{attrs:{tab:t.tab,tabs:t.tabsWithBadges,theme:"notice"}})],1)}),[],!1,gc,null,null,null);function gc(t){for(let e in fc)this[e]=fc[e]}var kc=function(){return mc.exports}();const vc={};var bc=Rt({inheritAttrs:!1},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-panel",{staticClass:"k-panel-inside",attrs:{tabindex:"0"}},[n("header",{staticClass:"k-panel-header"},[n("k-topbar",{attrs:{breadcrumb:t.$view.breadcrumb,license:t.$license,menu:t.$menu,view:t.$view}})],1),n("main",{staticClass:"k-panel-view scroll-y"},[t._t("default")],2),t._t("footer")],2)}),[],!1,yc,null,null,null);function yc(t){for(let e in vc)this[e]=vc[e]}var $c=function(){return bc.exports}(),_c=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("article",t._b({staticClass:"k-item",class:!!t.layout&&"k-"+t.layout+"-item",attrs:{"data-has-figure":t.hasFigure,"data-has-flag":Boolean(t.flag),"data-has-info":Boolean(t.info),"data-has-options":Boolean(t.options),tabindex:"-1"},on:{click:function(e){return t.$emit("click",e)},dragstart:function(e){return t.$emit("drag",e)}}},"article",t.data,!1),[t._t("image",(function(){return[t.hasFigure?n("k-item-image",{attrs:{image:t.image,layout:t.layout,width:t.width}}):t._e()]})),t.sortable?n("k-sort-handle",{staticClass:"k-item-sort-handle"}):t._e(),n("header",{staticClass:"k-item-content"},[t._t("default",(function(){return[n("h3",{staticClass:"k-item-title"},[!1!==t.link?n("k-link",{staticClass:"k-item-title-link",attrs:{target:t.target,to:t.link}},[n("span",{domProps:{innerHTML:t._s(t.title)}})]):n("span",{domProps:{innerHTML:t._s(t.title)}})],1),t.info?n("p",{staticClass:"k-item-info",domProps:{innerHTML:t._s(t.info)}}):t._e()]}))],2),t.flag||t.options||t.$slots.options?n("footer",{staticClass:"k-item-footer"},[n("nav",{staticClass:"k-item-buttons",on:{click:function(t){t.stopPropagation()}}},[t.flag?n("k-status-icon",t._b({},"k-status-icon",t.flag,!1)):t._e(),t._t("options",(function(){return[t.options?n("k-options-dropdown",{staticClass:"k-item-options-dropdown",attrs:{options:t.options},on:{option:t.onOption}}):t._e()]}))],2)]):t._e()],2)};const wc={inheritAttrs:!1,props:{data:Object,flag:Object,image:[Object,Boolean],info:String,layout:{type:String,default:"list"},link:{type:[Boolean,String,Function]},options:{type:[Array,Function,String]},sortable:Boolean,target:String,text:String,width:String},computed:{hasFigure(){return!1!==this.image&&Object.keys(this.image).length>0},title(){return this.text||"-"}},methods:{onOption(t){this.$emit("action",t),this.$emit("option",t)}}},xc={};var Sc=Rt(wc,_c,[],!1,Cc,null,null,null);function Cc(t){for(let e in xc)this[e]=xc[e]}var Oc=function(){return Sc.exports}();const Ec={inheritAttrs:!1,props:{image:[Object,Boolean],layout:{type:String,default:"list"},width:String},computed:{back(){return this.image.back||"black"},ratio(){return"cards"===this.layout&&this.image.ratio||"1/1"},size(){switch(this.layout){case"cards":return"large";case"cardlets":return"medium";default:return"regular"}},sizes(){switch(this.width){case"1/2":case"2/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 44em, 27em";case"1/3":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 29.333em, 27em";case"1/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 22em, 27em";case"2/3":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 27em, 27em";case"3/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 66em, 27em";default:return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 88em, 27em"}}}},Tc={};var Ac=Rt(Ec,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.image?n("div",{staticClass:"k-item-figure",style:{background:t.$helper.color(t.back)}},[t.image.src?n("k-image",{staticClass:"k-item-image",attrs:{cover:t.image.cover,ratio:t.ratio,sizes:t.sizes,src:t.image.src,srcset:t.image.srcset}}):n("k-aspect-ratio",{attrs:{ratio:t.ratio}},[n("k-icon",{staticClass:"k-item-icon",attrs:{color:t.$helper.color(t.image.color),size:t.size,type:t.image.icon}})],1)],1):t._e()}),[],!1,Mc,null,null,null);function Mc(t){for(let e in Tc)this[e]=Tc[e]}var Ic=function(){return Ac.exports}();const Lc={inheritAttrs:!1,props:{items:{type:Array,default:()=>[]},layout:{type:String,default:"list"},link:{type:Boolean,default:!0},image:{type:[Object,Boolean],default:()=>({})},sortable:Boolean,empty:{type:[String,Object]},size:{type:String,default:"default"}},computed:{dragOptions(){return{sort:this.sortable,disabled:!1===this.sortable,draggable:".k-draggable-item"}}},methods:{onDragStart(t,e){this.$store.dispatch("drag",{type:"text",data:e})},imageOptions(t){let e=this.image,n=t.image;return!1!==e&&!1!==n&&("object"!=typeof e&&(e={}),"object"!=typeof n&&(n={}),a(a({},n),e))}}},jc={};var Dc=Rt(Lc,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-draggable",{staticClass:"k-items",class:"k-"+t.layout+"-items",attrs:{handle:!0,options:t.dragOptions,"data-layout":t.layout,"data-size":t.size,list:t.items},on:{change:function(e){return t.$emit("change",e)},end:function(e){return t.$emit("sort",t.items,e)}}},[t._t("default",(function(){return t._l(t.items,(function(e,s){return n("k-item",t._b({key:e.id||s,class:{"k-draggable-item":t.sortable&&e.sortable},attrs:{image:t.imageOptions(e),layout:t.layout,link:!!t.link&&e.link,sortable:t.sortable&&e.sortable,width:e.column},on:{click:function(n){return t.$emit("item",e,s)},drag:function(n){return t.onDragStart(n,e.dragText)},flag:function(n){return t.$emit("flag",e,s)},option:function(n){return t.$emit("option",n,e,s)}},nativeOn:{mouseover:function(n){return t.$emit("hover",n,e,s)}},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options",null,{item:e,index:s})]},proxy:!0}],null,!0)},"k-item",e,!1))}))}))],2)}),[],!1,Bc,null,null,null);function Bc(t){for(let e in jc)this[e]=jc[e]}var Pc=function(){return Dc.exports}();const Nc={inheritAttrs:!0,props:{autofocus:{type:Boolean,default:!0},centered:{type:Boolean,default:!1},dimmed:{type:Boolean,default:!0},loading:{type:Boolean,default:!1}},data:()=>({isOpen:!1,scrollTop:0}),methods:{close(){!1!==this.isOpen&&(this.isOpen=!1,this.$emit("close"),this.restoreScrollPosition(),this.$events.$off("keydown.esc",this.close))},focus(){var t,e;let n=this.$refs.overlay.querySelector("\n [autofocus],\n [data-autofocus]\n ");return null===n&&(n=this.$refs.overlay.querySelector("\n input,\n textarea,\n select,\n button\n ")),"function"==typeof(null==n?void 0:n.focus)?n.focus():"function"==typeof(null==(e=null==(t=this.$slots.default[0])?void 0:t.context)?void 0:e.focus)?this.$slots.default[0].context.focus():void 0},open(){!0!==this.isOpen&&(this.storeScrollPosition(),this.isOpen=!0,this.$emit("open"),this.$events.$on("keydown.esc",this.close),setTimeout((()=>{!0===this.autofocus&&this.focus(),document.querySelector(".k-overlay > *").addEventListener("mousedown",(t=>t.stopPropagation())),this.$emit("ready")}),1))},restoreScrollPosition(){const t=document.querySelector(".k-panel-view");(null==t?void 0:t.scrollTop)&&(t.scrollTop=this.scrollTop)},storeScrollPosition(){const t=document.querySelector(".k-panel-view");(null==t?void 0:t.scrollTop)?this.scrollTop=t.scrollTop:this.scrollTop=0}}},qc={};var Rc=Rt(Nc,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isOpen?n("portal",[n("div",t._g({ref:"overlay",staticClass:"k-overlay",class:t.$vnode.data.staticClass,attrs:{"data-centered":t.loading||t.centered,"data-dimmed":t.dimmed,"data-loading":t.loading,dir:t.$translation.direction},on:{mousedown:t.close}},t.$listeners),[t.loading?n("k-loader",{staticClass:"k-overlay-loader"}):t._t("default",null,{close:t.close,isOpen:t.isOpen})],2)]):t._e()}),[],!1,Fc,null,null,null);function Fc(t){for(let e in qc)this[e]=qc[e]}var zc=function(){return Rc.exports}();const Yc={};var Hc=Rt({computed:{defaultLanguage(){return!!this.$language&&this.$language.default},dialog(){return this.$helper.clone(this.$store.state.dialog)},language(){return this.$language?this.$language.code:null},role(){return this.$user?this.$user.role:null},user(){return this.$user?this.$user.id:null}},created(){this.$events.$on("drop",this.drop)},destroyed(){this.$events.$off("drop",this.drop)},methods:{drop(){this.$store.dispatch("drag",null)}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-panel",attrs:{"data-dragging":t.$store.state.drag,"data-loading":t.$store.state.isLoading,"data-language":t.language,"data-language-default":t.defaultLanguage,"data-role":t.role,"data-translation":t.$translation.code,"data-user":t.user,dir:t.$translation.direction}},[t._t("default"),t.$store.state.dialog&&t.$store.state.dialog.props?[n("k-fiber-dialog",t._b({},"k-fiber-dialog",t.dialog,!1))]:t._e(),!1!==t.$store.state.fatal?n("k-fatal",{attrs:{html:t.$store.state.fatal}}):t._e(),!1===t.$system.isLocal?n("k-offline-warning"):t._e(),n("k-icons")],2)}),[],!1,Uc,null,null,null);function Uc(t){for(let e in Yc)this[e]=Yc[e]}var Kc=function(){return Hc.exports}();const Jc={};var Gc=Rt({props:{tab:String,tabs:Array,theme:String},data(){return{size:null,visibleTabs:this.tabs,invisibleTabs:[]}},computed:{current(){return(this.tabs.find((t=>t.name===this.tab))||this.tabs[0]||{}).name}},watch:{tabs(t){this.visibleTabs=t,this.invisibleTabs=[],this.resize(!0)}},created(){window.addEventListener("resize",this.resize)},destroyed(){window.removeEventListener("resize",this.resize)},methods:{resize(t){if(this.tabs&&!(this.tabs.length<=1)){if(this.tabs.length<=3)return this.visibleTabs=this.tabs,void(this.invisibleTabs=[]);if(window.innerWidth>=700){if("large"===this.size&&!t)return;this.visibleTabs=this.tabs,this.invisibleTabs=[],this.size="large"}else{if("small"===this.size&&!t)return;this.visibleTabs=this.tabs.slice(0,2),this.invisibleTabs=this.tabs.slice(2),this.size="small"}}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.tabs&&t.tabs.length>1?n("div",{staticClass:"k-tabs",attrs:{"data-theme":t.theme}},[n("nav",[t._l(t.visibleTabs,(function(e){return n("k-button",{key:e.name,staticClass:"k-tab-button",attrs:{link:e.link,current:t.current===e.name,icon:e.icon,tooltip:e.label}},[t._v(" "+t._s(e.label||e.text||e.name)+" "),e.badge?n("span",{staticClass:"k-tabs-badge"},[t._v(" "+t._s(e.badge)+" ")]):t._e()])})),t.invisibleTabs.length?n("k-button",{staticClass:"k-tab-button k-tabs-dropdown-button",attrs:{text:t.$t("more"),icon:"dots"},on:{click:function(e){return e.stopPropagation(),t.$refs.more.toggle()}}}):t._e()],2),t.invisibleTabs.length?n("k-dropdown-content",{ref:"more",staticClass:"k-tabs-dropdown",attrs:{align:"right"}},t._l(t.invisibleTabs,(function(e){return n("k-dropdown-item",{key:"more-"+e.name,attrs:{link:e.link,current:t.tab===e.name,icon:e.icon,tooltip:e.label}},[t._v(" "+t._s(e.label||e.text||e.name)+" ")])})),1):t._e()],1):t._e()}),[],!1,Vc,null,null,null);function Vc(t){for(let e in Jc)this[e]=Jc[e]}var Wc=function(){return Gc.exports}();const Xc={};var Zc=Rt({props:{align:String}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"k-view",attrs:{"data-align":t.align}},[t._t("default")],2)}),[],!1,Qc,null,null,null);function Qc(t){for(let e in Xc)this[e]=Xc[e]}var td=function(){return Zc.exports}();const ed={components:{draggable:J},props:{data:Object,element:String,handle:[String,Boolean],list:[Array,Object],move:Function,options:Object},data(){return{listeners:l(a({},this.$listeners),{start:t=>{this.$store.dispatch("drag",{}),this.$listeners.start&&this.$listeners.start(t)},end:t=>{this.$store.dispatch("drag",null),this.$listeners.end&&this.$listeners.end(t)}})}},computed:{dragOptions(){let t=!1;return t=!0===this.handle?".k-sort-handle":this.handle,a({fallbackClass:"k-sortable-fallback",fallbackOnBody:!0,forceFallback:!0,ghostClass:"k-sortable-ghost",handle:t,scroll:document.querySelector(".k-panel-view")},this.options)}}},nd={};var sd=Rt(ed,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("draggable",t._g(t._b({staticClass:"k-draggable",attrs:{"component-data":t.data,tag:t.element,list:t.list,move:t.move},scopedSlots:t._u([{key:"footer",fn:function(){return[t._t("footer")]},proxy:!0}],null,!0)},"draggable",t.dragOptions,!1),t.listeners),[t._t("default")],2)}),[],!1,id,null,null,null);function id(t){for(let e in nd)this[e]=nd[e]}var od=function(){return sd.exports}();const rd={};var ad=Rt({data:()=>({error:null}),errorCaptured(t){return this.$config.debug&&window.console.warn(t),this.error=t,!1},render(t){return this.error?this.$slots.error?this.$slots.error[0]:this.$scopedSlots.error?this.$scopedSlots.error({error:this.error}):t("k-box",{attrs:{theme:"negative"}},this.error.message||this.error):this.$slots.default[0]}},undefined,undefined,!1,ld,null,null,null);function ld(t){for(let e in rd)this[e]=rd[e]}var ud=function(){return ad.exports}();const cd={};var dd=Rt({props:{html:String},mounted(){try{let t=this.$refs.iframe.contentWindow.document;t.open(),t.write(this.html),t.close()}catch(t){console.error(t)}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-fatal"},[n("div",{staticClass:"k-fatal-box"},[n("k-bar",{scopedSlots:t._u([{key:"left",fn:function(){return[n("k-headline",[t._v(" The JSON response could not be parsed ")])]},proxy:!0},{key:"right",fn:function(){return[n("k-button",{attrs:{icon:"cancel",text:"Close"},on:{click:function(e){return t.$store.dispatch("fatal",!1)}}})]},proxy:!0}])}),n("iframe",{ref:"iframe",staticClass:"k-fatal-iframe"})],1)])}),[],!1,pd,null,null,null);function pd(t){for(let e in cd)this[e]=cd[e]}var hd=function(){return dd.exports}();const fd={};var md=Rt({props:{link:String,size:{type:String},tag:{type:String,default:"h2"},theme:{type:String}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n(t.tag,t._g({tag:"component",staticClass:"k-headline",attrs:{"data-theme":t.theme,"data-size":t.size}},t.$listeners),[t.link?n("k-link",{attrs:{to:t.link}},[t._t("default")],2):t._t("default")],2)}),[],!1,gd,null,null,null);function gd(t){for(let e in fd)this[e]=fd[e]}var kd=function(){return md.exports}();const vd={};var bd=Rt({props:{alt:String,color:String,back:String,size:String,type:String},computed:{isEmoji(){return this.$helper.string.hasEmoji(this.type)}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{class:"k-icon k-icon-"+t.type,style:{background:t.$helper.color(t.back)},attrs:{"aria-label":t.alt,role:t.alt?"img":null,"aria-hidden":!t.alt,"data-back":t.back,"data-size":t.size}},[t.isEmoji?n("span",{staticClass:"k-icon-emoji"},[t._v(t._s(t.type))]):n("svg",{style:{color:t.$helper.color(t.color)},attrs:{viewBox:"0 0 16 16"}},[n("use",{attrs:{"xlink:href":"#icon-"+t.type}})])])}),[],!1,yd,null,null,null);function yd(t){for(let e in vd)this[e]=vd[e]}var $d=function(){return bd.exports}(),_d=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("svg",{staticClass:"k-icons",attrs:{"aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg",overflow:"hidden"}},[n("defs",t._l(t.$options.icons,(function(e,s){return n("symbol",{key:s,attrs:{id:"icon-"+s,viewBox:"0 0 16 16"},domProps:{innerHTML:t._s(e)}})})),0)])},wd=[];const xd={icons:window.panel.plugins.icons},Sd={};var Cd=Rt(xd,_d,wd,!1,Od,null,null,null);function Od(t){for(let e in Sd)this[e]=Sd[e]}var Ed=function(){return Cd.exports}();const Td={props:{alt:String,back:String,cover:Boolean,ratio:String,sizes:String,src:String,srcset:String},data:()=>({loaded:{type:Boolean,default:!1},error:{type:Boolean,default:!1}}),computed:{ratioPadding(){return this.$helper.ratio(this.ratio||"1/1")}},created(){let t=new Image;t.onload=()=>{this.loaded=!0,this.$emit("load")},t.onerror=()=>{this.error=!0,this.$emit("error")},t.src=this.src}},Ad={};var Md=Rt(Td,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",t._g({staticClass:"k-image",attrs:{"data-ratio":t.ratio,"data-back":t.back,"data-cover":t.cover}},t.$listeners),[n("span",{style:"padding-bottom:"+t.ratioPadding},[t.loaded?n("img",{key:t.src,attrs:{alt:t.alt||"",src:t.src,srcset:t.srcset,sizes:t.sizes},on:{dragstart:function(t){t.preventDefault()}}}):t._e(),t.loaded||t.error?t._e():n("k-loader",{attrs:{position:"center",theme:"light"}}),!t.loaded&&t.error?n("k-icon",{staticClass:"k-image-error",attrs:{type:"cancel"}}):t._e()],1)])}),[],!1,Id,null,null,null);function Id(t){for(let e in Ad)this[e]=Ad[e]}var Ld=function(){return Md.exports}();const jd={};var Dd=Rt({},(function(){var t=this.$createElement,e=this._self._c||t;return e("span",{staticClass:"k-loader"},[e("k-icon",{staticClass:"k-loader-icon",attrs:{type:"loader"}})],1)}),[],!1,Bd,null,null,null);function Bd(t){for(let e in jd)this[e]=jd[e]}var Pd=function(){return Dd.exports}();const Nd={};var qd=Rt({data:()=>({offline:!1}),created(){this.$events.$on("offline",this.isOffline),this.$events.$on("online",this.isOnline)},destroyed(){this.$events.$off("offline",this.isOffline),this.$events.$off("online",this.isOnline)},methods:{isOnline(){this.offline=!1},isOffline(){this.offline=!0}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.offline?n("div",{staticClass:"k-offline-warning"},[n("p",[n("k-icon",{attrs:{type:"bolt"}}),t._v(" "+t._s(t.$t("error.offline")))],1)]):t._e()}),[],!1,Rd,null,null,null);function Rd(t){for(let e in Nd)this[e]=Nd[e]}var Fd=function(){return qd.exports}();const zd=(t,e=!1)=>{if(t>=0&&t<=100)return!0;if(e)throw new Error("value has to be between 0 and 100");return!1},Yd={};var Hd=Rt({props:{value:{type:Number,default:0,validator:zd}},data(){return{state:this.value}},watch:{value(t){this.state=t}},methods:{set(t){zd(t,!0),this.state=t}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("progress",{staticClass:"k-progress",attrs:{max:"100"},domProps:{value:t.state}},[t._v(t._s(t.state)+"%")])}),[],!1,Ud,null,null,null);function Ud(t){for(let e in Yd)this[e]=Yd[e]}var Kd=function(){return Hd.exports}();const Jd={};var Gd=Rt({},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-registration"},[n("p",[t._v(t._s(t.$t("license.unregistered")))]),n("k-button",{staticClass:"k-topbar-button",attrs:{responsive:!0,tooltip:t.$t("license.unregistered"),icon:"key"},on:{click:function(e){return t.$dialog("registration")}}},[t._v(" "+t._s(t.$t("license.register"))+" ")]),n("k-button",{staticClass:"k-topbar-button",attrs:{responsive:!0,link:"https://getkirby.com/buy",target:"_blank",icon:"cart"}},[t._v(" "+t._s(t.$t("license.buy"))+" ")])],1)}),[],!1,Vd,null,null,null);function Vd(t){for(let e in Jd)this[e]=Jd[e]}var Wd=function(){return Gd.exports}();const Xd={};var Zd=Rt({props:{icon:{type:String,default:"sort"}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-icon",{staticClass:"k-sort-handle",attrs:{type:t.icon,"aria-hidden":"true"}})}),[],!1,Qd,null,null,null);function Qd(t){for(let e in Xd)this[e]=Xd[e]}var tp=function(){return Zd.exports}();const ep={props:{click:{type:Function,default:()=>{}},disabled:Boolean,responsive:Boolean,status:String,text:String,tooltip:String},computed:{icon(){return"draft"===this.status?"circle-outline":"unlisted"===this.status?"circle-half":"circle"},theme(){return"draft"===this.status?"negative":"unlisted"===this.status?"info":"positive"},title(){let t=this.tooltip||this.text;return this.disabled&&(t+=` (${this.$t("disabled")})`),t}},methods:{onClick(){this.click(),this.$emit("click")}}},np={};var sp=Rt(ep,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-button",{class:"k-status-icon k-status-icon-"+t.status,attrs:{disabled:t.disabled,icon:t.icon,responsive:t.responsive,text:t.text,theme:t.theme,tooltip:t.title},on:{click:t.onClick}})}),[],!1,ip,null,null,null);function ip(t){for(let e in np)this[e]=np[e]}var op=function(){return sp.exports}();const rp={};var ap=Rt({props:{align:String,size:String,theme:String}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("div",{staticClass:"k-text",attrs:{"data-align":t.align,"data-size":t.size,"data-theme":t.theme}},[t._t("default")],2)}),[],!1,lp,null,null,null);function lp(t){for(let e in rp)this[e]=rp[e]}var up=function(){return ap.exports}();const cp={};var dp=Rt({props:{user:[Object,String]}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-user-info"},[t.user.avatar?n("k-image",{attrs:{cover:!0,src:t.user.avatar.url,ratio:"1/1"}}):n("k-icon",{attrs:{type:"user"}}),t._v(" "+t._s(t.user.name||t.user.email||t.user)+" ")],1)}),[],!1,pp,null,null,null);function pp(t){for(let e in cp)this[e]=cp[e]}var hp=function(){return dp.exports}();const fp={};var mp=Rt({props:{crumbs:{type:Array,default:()=>[]},label:{type:String,default:"Breadcrumb"},view:Object},computed:{dropdown(){return this.segments.map((t=>l(a({},t),{text:t.label,icon:"angle-right"})))},segments(){return[{link:this.view.link,label:this.view.breadcrumbLabel,icon:this.view.icon,loading:this.$store.state.isLoading},...this.crumbs]}},methods:{isLast(t){return this.crumbs.length-1===t}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("nav",{staticClass:"k-breadcrumb",attrs:{"aria-label":t.label}},[n("k-dropdown",{staticClass:"k-breadcrumb-dropdown"},[n("k-button",{attrs:{icon:"road-sign"},on:{click:function(e){return t.$refs.dropdown.toggle()}}}),n("k-dropdown-content",{ref:"dropdown",attrs:{options:t.dropdown,theme:"light"}})],1),n("ol",t._l(t.segments,(function(e,s){return n("li",{key:s},[n("k-link",{staticClass:"k-breadcrumb-link",attrs:{title:e.text||e.label,to:e.link,"aria-current":!!t.isLast(s)&&"page"}},[e.loading?n("k-loader",{staticClass:"k-breadcrumb-icon"}):e.icon?n("k-icon",{staticClass:"k-breadcrumb-icon",attrs:{type:e.icon}}):t._e(),n("span",{staticClass:"k-breadcrumb-link-text"},[t._v(" "+t._s(e.text||e.label)+" ")])],1)],1)})),0)],1)}),[],!1,gp,null,null,null);function gp(t){for(let e in fp)this[e]=fp[e]}var kp=function(){return mp.exports}();const vp={inheritAttrs:!1,props:{autofocus:Boolean,click:Function,current:[String,Boolean],disabled:Boolean,icon:String,id:[String,Number],link:String,responsive:Boolean,rel:String,role:String,target:String,tabindex:String,text:[String,Number],theme:String,tooltip:String,type:{type:String,default:"button"}},computed:{component(){return!0===this.disabled?"k-button-disabled":this.link?"k-button-link":"k-button-native"}},methods:{focus(){this.$refs.button.focus&&this.$refs.button.focus()},tab(){this.$refs.button.tab&&this.$refs.button.tab()},untab(){this.$refs.button.untab&&this.$refs.button.untab()}}},bp={};var yp=Rt(vp,(function(){var t=this,e=t.$createElement;return(t._self._c||e)(t.component,t._g(t._b({ref:"button",tag:"component"},"component",t.$props,!1),t.$listeners),[t.text?[t._v(" "+t._s(t.text)+" ")]:t._t("default")],2)}),[],!1,$p,null,null,null);function $p(t){for(let e in bp)this[e]=bp[e]}var _p=function(){return yp.exports}();const wp={inheritAttrs:!1,props:{icon:String,id:[String,Number],responsive:Boolean,theme:String,tooltip:String}},xp={};var Sp=Rt(wp,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{staticClass:"k-button",attrs:{id:t.id,"data-disabled":!0,"data-responsive":t.responsive,"data-theme":t.theme,title:t.tooltip}},[t.icon?n("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)}),[],!1,Cp,null,null,null);function Cp(t){for(let e in xp)this[e]=xp[e]}var Op=function(){return Sp.exports}();const Ep={};var Tp=Rt({props:{buttons:Array}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-button-group"},[t.$slots.default?t._t("default"):t._l(t.buttons,(function(e,s){return n("k-button",t._b({key:s},"k-button",e,!1))}))],2)}),[],!1,Ap,null,null,null);function Ap(t){for(let e in Ep)this[e]=Ep[e]}var Mp=function(){return Tp.exports}();const Ip={inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],icon:String,id:[String,Number],link:String,rel:String,responsive:Boolean,role:String,target:String,tabindex:String,theme:String,tooltip:String},methods:{focus(){this.$el.focus()}}},Lp={};var jp=Rt(Ip,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-link",t._g({staticClass:"k-button",attrs:{id:t.id,"aria-current":t.current,autofocus:t.autofocus,"data-theme":t.theme,"data-responsive":t.responsive,rel:t.rel,role:t.role,tabindex:t.tabindex,target:t.target,title:t.tooltip,to:t.link}},t.$listeners),[t.icon?n("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)}),[],!1,Dp,null,null,null);function Dp(t){for(let e in Lp)this[e]=Lp[e]}var Bp=function(){return jp.exports}(),Pp={mounted(){this.$el.addEventListener("keyup",this.onTab,!0),this.$el.addEventListener("blur",this.onUntab,!0)},destroyed(){this.$el.removeEventListener("keyup",this.onTab,!0),this.$el.removeEventListener("blur",this.onUntab,!0)},methods:{focus(){this.$el.focus&&this.$el.focus()},onTab(t){9===t.keyCode&&this.$el.setAttribute("data-tabbed",!0)},onUntab(){this.$el.removeAttribute("data-tabbed")},tab(){this.$el.focus(),this.$el.setAttribute("data-tabbed",!0)},untab(){this.$el.removeAttribute("data-tabbed")}}};const Np={mixins:[Pp],inheritAttrs:!1,props:{autofocus:Boolean,click:{type:Function,default:()=>{}},current:[String,Boolean],icon:String,id:[String,Number],responsive:Boolean,role:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}}},qp={};var Rp=Rt(Np,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("button",t._g({staticClass:"k-button",attrs:{id:t.id,"aria-current":t.current,autofocus:t.autofocus,"data-theme":t.theme,"data-responsive":t.responsive,role:t.role,tabindex:t.tabindex,title:t.tooltip,type:t.type},on:{click:t.click}},t.$listeners),[t.icon?n("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?n("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)}),[],!1,Fp,null,null,null);function Fp(t){for(let e in qp)this[e]=qp[e]}var zp=function(){return Rp.exports}();const Yp={};var Hp=Rt({},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("span",{staticClass:"k-dropdown",on:{click:function(t){t.stopPropagation()}}},[t._t("default")],2)}),[],!1,Up,null,null,null);function Up(t){for(let e in Yp)this[e]=Yp[e]}var Kp=function(){return Hp.exports}();let Jp=null;const Gp={};var Vp=Rt({props:{align:{type:String,default:"left"},options:[Array,Function,String],theme:{type:String,default:"dark"}},data:()=>({current:-1,dropup:!1,isOpen:!1,items:[]}),methods:{async fetchOptions(t){if(!this.options)return t(this.items);"string"==typeof this.options?this.$dropdown(this.options)(t):"function"==typeof this.options?this.options(t):Array.isArray(this.options)&&t(this.options)},onOptionClick(t){"function"==typeof t.click?t.click.call(this):t.click&&this.$emit("action",t.click)},open(){this.reset(),Jp&&Jp!==this&&Jp.close(),this.fetchOptions((t=>{this.$events.$on("keydown",this.navigate),this.$events.$on("click",this.close),this.items=t,this.isOpen=!0,Jp=this,this.onOpen(),this.$emit("open")}))},reset(){this.current=-1,this.$events.$off("keydown",this.navigate),this.$events.$off("click",this.close)},close(){this.reset(),this.isOpen=Jp=!1,this.$emit("close")},toggle(){this.isOpen?this.close():this.open()},focus(t=0){var e;(null==(e=this.$children[t])?void 0:e.focus)&&(this.current=t,this.$children[t].focus())},onOpen(){this.dropup=!1,this.$nextTick((()=>{if(this.$el){let t=window.innerHeight||document.body.clientHeight||document.documentElement.clientHeight,e=50,n=this.$el.getBoundingClientRect().top||0,s=this.$el.clientHeight;n+s>t-e&&s+2*ethis.$children.length-1){const t=this.$children.filter((t=>!1===t.disabled));this.current=this.$children.indexOf(t[t.length-1]);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled){this.focus(this.current);break}}break;case"Tab":for(;;){if(this.current++,this.current>this.$children.length-1){this.close(),this.$emit("leave",t.code);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled)break}}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isOpen?n("div",{staticClass:"k-dropdown-content",attrs:{"data-align":t.align,"data-dropup":t.dropup,"data-theme":t.theme}},[t._t("default",(function(){return[t._l(t.items,(function(e,s){return["-"===e?n("hr",{key:t._uid+"-item-"+s}):n("k-dropdown-item",t._b({key:t._uid+"-item-"+s,ref:t._uid+"-item-"+s,refInFor:!0,on:{click:function(n){return t.onOptionClick(e)}}},"k-dropdown-item",e,!1),[t._v(" "+t._s(e.text)+" ")])]}))]}))],2):t._e()}),[],!1,Wp,null,null,null);function Wp(t){for(let e in Gp)this[e]=Gp[e]}var Xp=function(){return Vp.exports}();const Zp={inheritAttrs:!1,props:{disabled:Boolean,icon:String,image:[String,Object],link:String,target:String,theme:String,upload:String,current:[String,Boolean]},data(){return{listeners:l(a({},this.$listeners),{click:t=>{this.$parent.close(),this.$emit("click",t)}})}},methods:{focus(){this.$refs.button.focus()},tab(){this.$refs.button.tab()}}},Qp={};var th=Rt(Zp,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-button",t._g(t._b({ref:"button",staticClass:"k-dropdown-item"},"k-button",t.$props,!1),t.listeners),[t._t("default")],2)}),[],!1,eh,null,null,null);function eh(t){for(let e in Qp)this[e]=Qp[e]}var nh=function(){return th.exports}();const sh={mixins:[Pp],props:{disabled:Boolean,rel:String,tabindex:[String,Number],target:String,title:String,to:[String,Function]},data(){return{relAttr:"_blank"===this.target?"noreferrer noopener":this.rel,listeners:l(a({},this.$listeners),{click:this.onClick})}},computed:{href(){return"function"==typeof this.to?"":"/"!==this.to[0]||this.target?this.to:this.$url(this.to)}},methods:{isRoutable(t){return!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)&&(!t.defaultPrevented&&((void 0===t.button||0===t.button)&&(!this.target&&!("string"==typeof this.href&&this.href.indexOf("://")>0))))},onClick(t){if(!0===this.disabled)return t.preventDefault(),!1;"function"==typeof this.to&&(t.preventDefault(),this.to()),this.isRoutable(t)&&(t.preventDefault(),this.$go(this.to)),this.$emit("click",t)}}},ih={};var oh=Rt(sh,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.to&&!t.disabled?n("a",t._g({ref:"link",staticClass:"k-link",attrs:{href:t.href,rel:t.relAttr,tabindex:t.tabindex,target:t.target,title:t.title}},t.listeners),[t._t("default")],2):n("span",{staticClass:"k-link",attrs:{title:t.title,"data-disabled":""}},[t._t("default")],2)}),[],!1,rh,null,null,null);function rh(t){for(let e in ih)this[e]=ih[e]}var ah=function(){return oh.exports}();const lh={};var uh=Rt({computed:{defaultLanguage(){return this.$languages.find((t=>!0===t.default))},language(){return this.$language},languages(){return this.$languages.filter((t=>!1===t.default))}},methods:{change(t){this.$emit("change",t),this.$go(window.location,{query:{language:t.code}})}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.languages.length?n("k-dropdown",{staticClass:"k-languages-dropdown"},[n("k-button",{attrs:{text:t.language.name,responsive:!0,icon:"globe"},on:{click:function(e){return t.$refs.languages.toggle()}}}),t.languages?n("k-dropdown-content",{ref:"languages"},[n("k-dropdown-item",{on:{click:function(e){return t.change(t.defaultLanguage)}}},[t._v(" "+t._s(t.defaultLanguage.name)+" ")]),n("hr"),t._l(t.languages,(function(e){return n("k-dropdown-item",{key:e.code,on:{click:function(n){return t.change(e)}}},[t._v(" "+t._s(e.name)+" ")])}))],2):t._e()],1):t._e()}),[],!1,ch,null,null,null);function ch(t){for(let e in lh)this[e]=lh[e]}var dh=function(){return uh.exports}();const ph={props:{align:{type:String,default:"right"},icon:{type:String,default:"dots"},options:{type:[Array,Function,String],default:()=>[]},text:{type:[Boolean,String],default:!0},theme:{type:String,default:"dark"}},computed:{hasSingleOption(){return Array.isArray(this.options)&&1===this.options.length}},methods:{onAction(t,e,n){this.$emit("action",t,e,n),this.$emit("option",t,e,n)},toggle(){this.$refs.options.toggle()}}},hh={};var fh=Rt(ph,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.hasSingleOption?n("k-button",t._b({staticClass:"k-options-dropdown-toggle",attrs:{icon:t.options[0].icon||t.icon,tooltip:t.options[0].tooltip||t.options[0].text},on:{click:function(e){return t.onAction(t.options[0].option||t.options[0].click,t.options[0],0)}}},"k-button",t.options[0],!1),[!0===t.text?[t._v(" "+t._s(t.options[0].text)+" ")]:!1!==t.text?[t._v(" "+t._s(t.text)+" ")]:t._e()],2):t.options.length?n("k-dropdown",{staticClass:"k-options-dropdown"},[n("k-button",{staticClass:"k-options-dropdown-toggle",attrs:{icon:t.icon,tooltip:t.$t("options")},on:{click:function(e){return t.$refs.options.toggle()}}},[t.text&&!0!==t.text?[t._v(" "+t._s(t.text)+" ")]:t._e()],2),n("k-dropdown-content",{ref:"options",staticClass:"k-options-dropdown-content",attrs:{align:t.align,options:t.options},on:{action:t.onAction}})],1):t._e()}),[],!1,mh,null,null,null);function mh(t){for(let e in hh)this[e]=hh[e]}var gh=function(){return fh.exports}();const kh={props:{align:{type:String,default:"left"},details:{type:Boolean,default:!1},dropdown:{type:Boolean,default:!0},keys:{type:Boolean,default:!1},limit:{type:Number,default:10},page:{type:Number,default:1},pageLabel:{type:String,default:()=>window.panel.$t("pagination.page")},total:{type:Number,default:0},prevLabel:{type:String,default:()=>window.panel.$t("prev")},nextLabel:{type:String,default:()=>window.panel.$t("next")},validate:{type:Function,default:()=>Promise.resolve()}},data(){return{currentPage:this.page}},computed:{show(){return this.pages>1},start(){return(this.currentPage-1)*this.limit+1},end(){let t=this.start-1+this.limit;return t>this.total?this.total:t},detailsText(){return 1===this.limit?this.start+" / ":this.start+"-"+this.end+" / "},pages(){return Math.ceil(this.total/this.limit)},hasPrev(){return this.start>1},hasNext(){return this.endthis.limit},offset(){return this.start-1}},watch:{page(t){this.currentPage=parseInt(t)}},created(){!0===this.keys&&window.addEventListener("keydown",this.navigate,!1)},destroyed(){window.removeEventListener("keydown",this.navigate,!1)},methods:{async goTo(t){try{await this.validate(t),t<1&&(t=1),t>this.pages&&(t=this.pages),this.currentPage=t,this.$refs.dropdown&&this.$refs.dropdown.close(),this.$emit("paginate",{page:this.currentPage,start:this.start,end:this.end,limit:this.limit,offset:this.offset})}catch(e){}},prev(){this.goTo(this.currentPage-1)},next(){this.goTo(this.currentPage+1)},navigate(t){switch(t.code){case"ArrowLeft":this.prev();break;case"ArrowRight":this.next()}}}},vh={};var bh=Rt(kh,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.show?n("nav",{staticClass:"k-pagination",attrs:{"data-align":t.align}},[t.show?n("k-button",{attrs:{disabled:!t.hasPrev,tooltip:t.prevLabel,icon:"angle-left"},on:{click:t.prev}}):t._e(),t.details?[t.dropdown?[n("k-dropdown",[n("k-button",{staticClass:"k-pagination-details",attrs:{disabled:!t.hasPages},on:{click:function(e){return t.$refs.dropdown.toggle()}}},[t.total>1?[t._v(" "+t._s(t.detailsText)+" ")]:t._e(),t._v(" "+t._s(t.total)+" ")],2),n("k-dropdown-content",{ref:"dropdown",staticClass:"k-pagination-selector",on:{open:function(e){t.$nextTick((function(){return t.$refs.page.focus()}))}}},[n("div",{staticClass:"k-pagination-settings"},[n("label",{attrs:{for:"k-pagination-page"}},[n("span",[t._v(t._s(t.pageLabel)+":")]),n("select",{ref:"page",attrs:{id:"k-pagination-page"}},t._l(t.pages,(function(e){return n("option",{key:e,domProps:{selected:t.page===e,value:e}},[t._v(" "+t._s(e)+" ")])})),0)]),n("k-button",{attrs:{icon:"check"},on:{click:function(e){return t.goTo(t.$refs.page.value)}}})],1)])],1)]:[n("span",{staticClass:"k-pagination-details"},[t.total>1?[t._v(" "+t._s(t.detailsText)+" ")]:t._e(),t._v(" "+t._s(t.total)+" ")],2)]]:t._e(),t.show?n("k-button",{attrs:{disabled:!t.hasNext,tooltip:t.nextLabel,icon:"angle-right"},on:{click:t.next}}):t._e()],2):t._e()}),[],!1,yh,null,null,null);function yh(t){for(let e in vh)this[e]=vh[e]}var $h=function(){return bh.exports}();const _h={props:{prev:{type:[Boolean,Object],default:!1},next:{type:[Boolean,Object],default:!1}},computed:{buttons(){return[l(a({},this.button(this.prev)),{icon:"angle-left"}),l(a({},this.button(this.next)),{icon:"angle-right"})]}},methods:{button:t=>t||{disabled:!0,link:"#"}}},wh={};var xh=Rt(_h,(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-button-group",{staticClass:"k-prev-next",attrs:{buttons:t.buttons}})}),[],!1,Sh,null,null,null);function Sh(t){for(let e in wh)this[e]=wh[e]}var Ch=function(){return xh.exports}();const Oh={};var Eh=Rt({props:{types:{type:Object,default:()=>({})},type:String},data(){return{isLoading:!1,hasResults:!0,items:[],currentType:this.getType(this.type),q:null,selected:-1}},watch:{q(t,e){t!==e&&this.search(this.q)},currentType(t,e){t!==e&&this.search(this.q)},type(){this.currentType=this.getType(this.type)}},created(){this.search=$t(this.search,250),this.$events.$on("keydown.cmd.shift.f",this.open)},destroyed(){this.$events.$off("keydown.cmd.shift.f",this.open)},methods:{changeType(t){this.currentType=this.getType(t),this.$nextTick((()=>{this.$refs.input.focus()}))},close(){this.$refs.overlay.close(),this.hasResults=!0,this.items=[],this.q=null},getType(t){return this.types[t]||this.types[Object.keys(this.types)[0]]},navigate(t){this.$go(t.link),this.close()},onDown(){this.selected=0&&this.select(this.selected-1)},open(){this.$refs.overlay.open()},async search(t){this.isLoading=!0,this.$refs.types&&this.$refs.types.close();try{if(null===t||""===t)throw Error("Empty query");const e=await this.$search(this.currentType.id,t);if(!1===e)throw Error("JSON parsing failed");this.items=e.results}catch(e){this.items=[]}finally{this.select(-1),this.isLoading=!1,this.hasResults=this.items.length>0}},select(t){if(this.selected=t,this.$refs.items){const e=this.$refs.items.$el.querySelectorAll(".k-item");[...e].forEach((t=>delete t.dataset.selected)),t>=0&&(e[t].dataset.selected=!0)}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-overlay",{ref:"overlay"},[n("div",{staticClass:"k-search",attrs:{role:"search"}},[n("div",{staticClass:"k-search-input"},[n("k-dropdown",{staticClass:"k-search-types"},[n("k-button",{attrs:{icon:t.currentType.icon,text:t.currentType.label},on:{click:function(e){return t.$refs.types.toggle()}}}),n("k-dropdown-content",{ref:"types"},t._l(t.types,(function(e,s){return n("k-dropdown-item",{key:s,attrs:{icon:e.icon},on:{click:function(e){return t.changeType(s)}}},[t._v(" "+t._s(e.label)+" ")])})),1)],1),n("input",{directives:[{name:"model",rawName:"v-model",value:t.q,expression:"q"}],ref:"input",attrs:{placeholder:t.$t("search")+" …","aria-label":t.$t("search"),autofocus:!0,type:"text"},domProps:{value:t.q},on:{input:[function(e){e.target.composing||(t.q=e.target.value)},function(e){t.hasResults=!0}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.preventDefault(),t.onDown.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.preventDefault(),t.onUp.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:(e.preventDefault(),t.onTab.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.onEnter.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:t.close.apply(null,arguments)}]}}),n("k-button",{staticClass:"k-search-close",attrs:{icon:t.isLoading?"loader":"cancel",tooltip:t.$t("close")},on:{click:t.close}})],1),!t.q||t.hasResults&&!t.items.length?t._e():n("div",{staticClass:"k-search-results"},[t.items.length?n("k-items",{ref:"items",attrs:{items:t.items},on:{hover:t.onHover},nativeOn:{mouseout:function(e){return t.select(-1)}}}):t.hasResults?t._e():n("p",{staticClass:"k-search-empty"},[t._v(" "+t._s(t.$t("search.results.none"))+" ")])],1)])])}),[],!1,Th,null,null,null);function Th(t){for(let e in Oh)this[e]=Oh[e]}var Ah=function(){return Eh.exports}();const Mh={props:{removable:Boolean},methods:{remove(){this.removable&&this.$emit("remove")},focus(){this.$refs.button.focus()}}},Ih={};var Lh=Rt(Mh,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("span",{ref:"button",staticClass:"k-tag",attrs:{tabindex:"0"},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.remove.apply(null,arguments))}}},[n("span",{staticClass:"k-tag-text"},[t._t("default")],2),t.removable?n("span",{staticClass:"k-tag-toggle",on:{click:t.remove}},[t._v("×")]):t._e()])}),[],!1,jh,null,null,null);function jh(t){for(let e in Ih)this[e]=Ih[e]}var Dh=function(){return Lh.exports}();const Bh={props:{breadcrumb:Array,license:Boolean,menu:Array,title:String,view:Object},computed:{notification(){return this.$store.state.notification.type&&"error"!==this.$store.state.notification.type?this.$store.state.notification:null}}},Ph={};var Nh=Rt(Bh,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-topbar"},[n("k-view",[n("div",{staticClass:"k-topbar-wrapper"},[n("k-dropdown",{staticClass:"k-topbar-menu"},[n("k-button",{staticClass:"k-topbar-button k-topbar-menu-button",attrs:{tooltip:t.$t("menu"),icon:"bars"},on:{click:function(e){return t.$refs.menu.toggle()}}},[n("k-icon",{attrs:{type:"angle-down"}})],1),n("k-dropdown-content",{ref:"menu",staticClass:"k-topbar-menu",attrs:{options:t.menu,theme:"light"}})],1),n("k-breadcrumb",{staticClass:"k-topbar-breadcrumb",attrs:{crumbs:t.breadcrumb,view:t.view}}),n("div",{staticClass:"k-topbar-signals"},[t.notification?n("k-button",{staticClass:"k-topbar-notification k-topbar-button",attrs:{text:t.notification.message,theme:"positive"},on:{click:function(e){return t.$store.dispatch("notification/close")}}}):t.license?t._e():n("k-registration"),n("k-form-indicator"),n("k-button",{staticClass:"k-topbar-button",attrs:{tooltip:t.$t("search"),icon:"search"},on:{click:function(e){return t.$refs.search.open()}}})],1)],1)]),n("k-search",{ref:"search",attrs:{type:t.$view.search||"pages",types:t.$searches}})],1)}),[],!1,qh,null,null,null);function qh(t){for(let e in Ph)this[e]=Ph[e]}var Rh=function(){return Nh.exports}();const Fh={props:{empty:String,blueprint:String,lock:[Boolean,Object],parent:String,tab:Object},computed:{content(){return this.$store.getters["content/values"]()}},methods:{exists(t){return this.$helper.isComponent(`k-${t}-section`)},meetsCondition(t){if(!t.when)return!0;let e=!0;return Object.keys(t.when).forEach((n=>{this.content[n.toLowerCase()]!==t.when[n]&&(e=!1)})),e}}},zh={};var Yh=Rt(Fh,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return 0===t.tab.columns.length?n("k-box",{attrs:{html:!0,text:t.empty,theme:"info"}}):n("k-grid",{staticClass:"k-sections",attrs:{gutter:"large"}},t._l(t.tab.columns,(function(e,s){return n("k-column",{key:t.parent+"-column-"+s,attrs:{width:e.width,sticky:e.sticky}},[t._l(e.sections,(function(i,o){return[t.meetsCondition(i)?[t.exists(i.type)?n("k-"+i.type+"-section",t._b({key:t.parent+"-column-"+s+"-section-"+o+"-"+t.blueprint,tag:"component",class:"k-section k-section-name-"+i.name,attrs:{column:e.width,lock:t.lock,name:i.name,parent:t.parent,timestamp:t.$view.timestamp},on:{submit:function(e){return t.$emit("submit",e)}}},"component",i,!1)):[n("k-box",{key:t.parent+"-column-"+s+"-section-"+o,attrs:{text:t.$t("error.section.type.invalid",{type:i.type}),theme:"negative"}})]]:t._e()]}))],2)})),1)}),[],!1,Hh,null,null,null);function Hh(t){for(let e in zh)this[e]=zh[e]}var Uh=function(){return Yh.exports}();const Kh={};var Jh=Rt({mixins:[Nt],data:()=>({headline:null,text:null,theme:null}),async created(){const t=await this.load();this.headline=t.headline,this.text=t.text,this.theme=t.theme||"info"}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("section",{staticClass:"k-info-section"},[n("k-headline",{staticClass:"k-info-section-headline"},[t._v(" "+t._s(t.headline)+" ")]),n("k-box",{attrs:{theme:t.theme}},[n("k-text",{domProps:{innerHTML:t._s(t.text)}})],1)],1)}),[],!1,Gh,null,null,null);function Gh(t){for(let e in Kh)this[e]=Kh[e]}var Vh=function(){return Jh.exports}(),Wh={inheritAttrs:!1,props:{blueprint:String,column:String,parent:String,name:String,timestamp:Number},data:()=>({data:[],error:null,isLoading:!1,isProcessing:!1,options:{empty:null,headline:null,help:null,layout:"list",link:null,max:null,min:null,size:null,sortable:null},pagination:{page:null}}),computed:{headline(){return this.options.headline||" "},help(){return this.options.help},isInvalid(){return!!(this.options.min&&this.data.lengththis.options.max)},paginationId(){return"kirby$pagination$"+this.parent+"/"+this.name}},watch:{timestamp(){this.reload()}},methods:{items:t=>t,async load(t){t||(this.isLoading=!0),this.isProcessing=!0,null===this.pagination.page&&(this.pagination.page=localStorage.getItem(this.paginationId)||1);try{const t=await this.$api.get(this.parent+"/sections/"+this.name,{page:this.pagination.page});this.options=t.options,this.pagination=t.pagination,this.data=this.items(t.data)}catch(e){this.error=e.message}finally{this.isProcessing=!1,this.isLoading=!1}},paginate(t){localStorage.setItem(this.paginationId,t.page),this.pagination=t,this.reload()},async reload(){await this.load(!0)}}};const Xh={};var Zh=Rt({mixins:[Wh],computed:{add(){return this.options.add&&this.$permissions.pages.create}},created(){this.load(),this.$events.$on("page.changeStatus",this.reload),this.$events.$on("page.sort",this.reload)},destroyed(){this.$events.$off("page.changeStatus",this.reload),this.$events.$off("page.sort",this.reload)},methods:{create(){this.add&&this.$dialog("pages/create",{query:{parent:this.options.link||this.parent,view:this.parent,section:this.name}})},items(t){return t.map((e=>{const n=!1!==e.permissions.changeStatus;return e.flag={status:e.status,tooltip:this.$t("page.status"),disabled:!n,click:()=>{this.$dialog(e.link+"/changeStatus")}},e.sortable=e.permissions.sort&&this.options.sortable,e.deletable=t.length>this.options.min,e.column=this.column,e.options=this.$dropdown(e.link,{query:{view:"list",delete:e.deletable,sort:e.sortable}}),e.data={"data-id":e.id,"data-status":e.status,"data-template":e.template},e}))},async sort(t){let e=null;if(t.added&&(e="added"),t.moved&&(e="moved"),e){this.isProcessing=!0;const s=t[e].element,i=t[e].newIndex+1+this.pagination.offset;try{await this.$api.pages.status(s.id,"listed",i),this.$store.dispatch("notification/success",":)"),this.$events.$emit("page.sort",s)}catch(n){this.$store.dispatch("notification/error",{message:n.message,details:n.details}),await this.reload()}finally{this.isProcessing=!1}}},update(){this.reload(),this.$events.$emit("model.update")}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return!1===t.isLoading?n("section",{staticClass:"k-pages-section",attrs:{"data-processing":t.isProcessing}},[n("header",{staticClass:"k-section-header"},[n("k-headline",{attrs:{link:t.options.link}},[t._v(" "+t._s(t.headline)+" "),t.options.min?n("abbr",{attrs:{title:t.$t("section.required")}},[t._v("*")]):t._e()]),t.add?n("k-button-group",{attrs:{buttons:[{text:t.$t("add"),icon:"add",click:t.create}]}}):t._e()],1),t.error?[n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[n("strong",[t._v(" "+t._s(t.$t("error.section.notLoaded",{name:t.name}))+": ")]),t._v(" "+t._s(t.error)+" ")])],1)]:[t.data.length?n("k-collection",{attrs:{layout:t.options.layout,help:t.help,items:t.data,pagination:t.pagination,sortable:!t.isProcessing&&t.options.sortable,size:t.options.size,"data-invalid":t.isInvalid},on:{change:t.sort,paginate:t.paginate}}):[n("k-empty",t._g({attrs:{layout:t.options.layout,"data-invalid":t.isInvalid,icon:"page"}},t.add?{click:t.create}:{}),[t._v(" "+t._s(t.options.empty||t.$t("pages.empty"))+" ")]),n("footer",{staticClass:"k-collection-footer"},[t.help?n("k-text",{staticClass:"k-collection-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()],1)]]],2):t._e()}),[],!1,Qh,null,null,null);function Qh(t){for(let e in Xh)this[e]=Xh[e]}var tf=function(){return Zh.exports}();const ef={};var nf=Rt({mixins:[Wh],computed:{add(){return!(!this.$permissions.files.create||!1===this.options.upload)&&this.options.upload}},created(){this.load(),this.$events.$on("model.update",this.reload),this.$events.$on("file.sort",this.reload)},destroyed(){this.$events.$off("model.update",this.reload),this.$events.$off("file.sort",this.reload)},methods:{action(t,e){"replace"===t&&this.replace(e)},drop(t){if(!1===this.add)return!1;this.$refs.upload.drop(t,l(a({},this.add),{url:this.$urls.api+"/"+this.add.api}))},items(t){return t.map((e=>(e.sortable=this.options.sortable,e.column=this.column,e.options=this.$dropdown(e.link,{query:{view:"list",update:this.options.sortable,delete:t.length>this.options.min}}),e.data={"data-id":e.id,"data-template":e.template},e)))},replace(t){this.$refs.upload.open({url:this.$urls.api+"/"+t.link,accept:"."+t.extension+","+t.mime,multiple:!1})},async sort(t){if(!1===this.options.sortable)return!1;this.isProcessing=!0,t=t.map((t=>t.id));try{await this.$api.patch(this.options.apiUrl+"/files/sort",{files:t,index:this.pagination.offset}),this.$store.dispatch("notification/success",":)"),this.$events.$emit("file.sort")}catch(e){this.reload(),this.$store.dispatch("notification/error",e.message)}finally{this.isProcessing=!1}},update(){this.$events.$emit("model.update")},upload(){if(!1===this.add)return!1;this.$refs.upload.open(l(a({},this.add),{url:this.$urls.api+"/"+this.add.api}))},uploaded(){this.$events.$emit("file.create"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return!1===t.isLoading?n("section",{staticClass:"k-files-section",attrs:{"data-processing":t.isProcessing}},[n("header",{staticClass:"k-section-header"},[n("k-headline",[t._v(" "+t._s(t.headline)+" "),t.options.min?n("abbr",{attrs:{title:t.$t("section.required")}},[t._v("*")]):t._e()]),t.add?n("k-button-group",{attrs:{buttons:[{text:t.$t("add"),icon:"upload",click:t.upload}]}}):t._e()],1),t.error?[n("k-box",{attrs:{theme:"negative"}},[n("k-text",{attrs:{size:"small"}},[n("strong",[t._v(t._s(t.$t("error.section.notLoaded",{name:t.name}))+":")]),t._v(" "+t._s(t.error)+" ")])],1)]:[n("k-dropzone",{attrs:{disabled:!1===t.add},on:{drop:t.drop}},[t.data.length?n("k-collection",{attrs:{help:t.help,items:t.data,layout:t.options.layout,pagination:t.pagination,sortable:!t.isProcessing&&t.options.sortable,size:t.options.size,"data-invalid":t.isInvalid},on:{sort:t.sort,paginate:t.paginate,action:t.action}}):[n("k-empty",t._g({attrs:{layout:t.options.layout,"data-invalid":t.isInvalid,icon:"image"}},t.add?{click:t.upload}:{}),[t._v(" "+t._s(t.options.empty||t.$t("files.empty"))+" ")]),n("footer",{staticClass:"k-collection-footer"},[t.help?n("k-text",{staticClass:"k-collection-help",attrs:{theme:"help"},domProps:{innerHTML:t._s(t.help)}}):t._e()],1)]],2),n("k-upload",{ref:"upload",on:{success:t.uploaded,error:t.reload}})]],2):t._e()}),[],!1,sf,null,null,null);function sf(t){for(let e in ef)this[e]=ef[e]}var of=function(){return nf.exports}();const rf={};var af=Rt({mixins:[Nt],inheritAttrs:!1,data:()=>({fields:{},isLoading:!0,issue:null}),computed:{values(){return this.$store.getters["content/values"]()}},watch:{timestamp(){this.fetch()}},created(){this.input=$t(this.input,50),this.fetch()},methods:{input(t,e,n){this.$store.dispatch("content/update",[n,t[n]])},async fetch(){try{const t=await this.load();this.fields=t.fields,Object.keys(this.fields).forEach((t=>{this.fields[t].section=this.name,this.fields[t].endpoints={field:this.parent+"/fields/"+t,section:this.parent+"/sections/"+this.name,model:this.parent}}))}catch(t){this.issue=t}finally{this.isLoading=!1}},onSubmit(t){this.$events.$emit("keydown.cmd.s",t)}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return t.isLoading?t._e():n("section",{staticClass:"k-fields-section"},[t.issue?[n("k-headline",{staticClass:"k-fields-issue-headline"},[t._v(" Error ")]),n("k-box",{attrs:{text:t.issue.message,html:!1,theme:"negative"}})]:t._e(),n("k-form",{attrs:{fields:t.fields,validate:!0,value:t.values,disabled:t.lock&&"lock"===t.lock.state},on:{input:t.input,submit:t.onSubmit}})],2)}),[],!1,lf,null,null,null);function lf(t){for(let e in rf)this[e]=rf[e]}var uf=function(){return af.exports}();const cf={props:{blueprint:String,next:Object,prev:Object,permissions:{type:Object,default:()=>({})},lock:{type:[Boolean,Object]},model:{type:Object,default:()=>({})},tab:{type:Object,default:()=>({columns:[]})},tabs:{type:Array,default:()=>[]}},computed:{id(){return this.model.link},isLocked(){var t;return"lock"===(null==(t=this.lock)?void 0:t.state)}},watch:{"model.id":{handler(){this.content()},immediate:!0}},created(){this.$events.$on("model.reload",this.reload),this.$events.$on("keydown.left",this.toPrev),this.$events.$on("keydown.right",this.toNext)},destroyed(){this.$events.$off("model.reload",this.reload),this.$events.$off("keydown.left",this.toPrev),this.$events.$off("keydown.right",this.toNext)},methods:{content(){this.$store.dispatch("content/create",{id:this.id,api:this.id,content:this.model.content})},async reload(){await this.$reload(),this.content()},toPrev(t){this.prev&&"body"===t.target.localName&&this.$go(this.prev.link)},toNext(t){this.next&&"body"===t.target.localName&&this.$go(this.next.link)}}};const df={};var pf=Rt(cf,undefined,undefined,!1,hf,null,null,null);function hf(t){for(let e in df)this[e]=df[e]}var ff=function(){return pf.exports}();const mf={};var gf=Rt({extends:ff,computed:{avatarOptions(){return[{icon:"upload",text:this.$t("change"),click:()=>this.$refs.upload.open()},{icon:"trash",text:this.$t("delete"),click:this.deleteAvatar}]},buttons(){return[{icon:"email",text:`${this.$t("email")}: ${this.model.email}`,disabled:!this.permissions.changeEmail||this.isLocked,click:()=>this.$dialog(this.id+"/changeEmail")},{icon:"bolt",text:`${this.$t("role")}: ${this.model.role}`,disabled:!this.permissions.changeRole||this.isLocked,click:()=>this.$dialog(this.id+"/changeRole")},{icon:"globe",text:`${this.$t("language")}: ${this.model.language}`,disabled:!this.permissions.changeLanguage||this.isLocked,click:()=>this.$dialog(this.id+"/changeLanguage")}]},uploadApi(){return this.$urls.api+"/"+this.id+"/avatar"}},methods:{async deleteAvatar(){await this.$api.users.deleteAvatar(this.model.id),this.avatar=null,this.$store.dispatch("notification/success",":)"),this.$reload()},onAvatar(){this.model.avatar?this.$refs.picture.toggle():this.$refs.upload.open()},uploadedAvatar(){this.$store.dispatch("notification/success",":)"),this.$reload()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[n("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[n("div",{staticClass:"k-user-view",attrs:{"data-locked":t.isLocked,"data-id":t.model.id,"data-template":t.blueprint}},[n("div",{staticClass:"k-user-profile"},[n("k-view",[n("k-dropdown",[n("k-button",{staticClass:"k-user-view-image",attrs:{tooltip:t.$t("avatar"),disabled:t.isLocked},on:{click:t.onAvatar}},[t.model.avatar?n("k-image",{attrs:{cover:!0,src:t.model.avatar,ratio:"1/1"}}):n("k-icon",{attrs:{back:"gray-900",color:"gray-200",type:"user"}})],1),t.model.avatar?n("k-dropdown-content",{ref:"picture",attrs:{options:t.avatarOptions}}):t._e()],1),n("k-button-group",{attrs:{buttons:t.buttons}})],1)],1),n("k-view",[n("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tab:t.tab.name,tabs:t.tabs},on:{edit:function(e){return t.$dialog(t.id+"/changeName")}},scopedSlots:t._u([{key:"left",fn:function(){return[n("k-button-group",[n("k-dropdown",{staticClass:"k-user-view-options"},[n("k-button",{attrs:{disabled:t.isLocked,text:t.$t("settings"),icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}}),n("k-dropdown-content",{ref:"settings",attrs:{options:t.$dropdown(t.id)}})],1),n("k-languages-dropdown")],1)]},proxy:!0},{key:"right",fn:function(){return[t.model.account?t._e():n("k-prev-next",{attrs:{prev:t.prev,next:t.next}})]},proxy:!0}])},[t.model.name&&0!==t.model.name.length?[t._v(" "+t._s(t.model.name)+" ")]:n("span",{staticClass:"k-user-name-placeholder"},[t._v(" "+t._s(t.$t("name"))+" … ")])],2),n("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("user.blueprint",{blueprint:t.$esc(t.blueprint)}),lock:t.lock,parent:t.id,tab:t.tab}}),n("k-upload",{ref:"upload",attrs:{url:t.uploadApi,multiple:!1,accept:"image/*"},on:{success:t.uploadedAvatar}})],1)],1)])}),[],!1,kf,null,null,null);function kf(t){for(let e in mf)this[e]=mf[e]}var vf=function(){return gf.exports}();const bf={};var yf=Rt({extends:vf,prevnext:!1},undefined,undefined,!1,$f,null,null,null);function $f(t){for(let e in bf)this[e]=bf[e]}var _f=function(){return yf.exports}();const wf={};var xf=Rt({props:{error:String,layout:String}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-"+t.layout,{tag:"component"},[n("k-view",{staticClass:"k-error-view"},[n("div",{staticClass:"k-error-view-content"},[n("k-text",[n("p",[n("k-icon",{staticClass:"k-error-view-icon",attrs:{type:"alert"}})],1),t._t("default",(function(){return[n("p",[t._v(" "+t._s(t.error)+" ")])]}))],2)],1)])],1)}),[],!1,Sf,null,null,null);function Sf(t){for(let e in wf)this[e]=wf[e]}var Cf=function(){return xf.exports}();const Of={};var Ef=Rt({extends:ff,props:{preview:Object},methods:{action(t){if("replace"===t)this.$refs.upload.open({url:this.$urls.api+"/"+this.id,accept:"."+this.model.extension+","+this.model.mime,multiple:!1})},onUpload(){this.$store.dispatch("notification/success",":)"),this.$reload()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[n("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[n("div",{staticClass:"k-file-view",attrs:{"data-locked":t.isLocked,"data-id":t.model.id,"data-template":t.blueprint}},[n("k-file-preview",t._b({},"k-file-preview",t.preview,!1)),n("k-view",{staticClass:"k-file-content"},[n("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tab:t.tab.name,tabs:t.tabs},on:{edit:function(e){return t.$dialog(t.id+"/changeName")}},scopedSlots:t._u([{key:"left",fn:function(){return[n("k-button-group",[n("k-button",{staticClass:"k-file-view-options",attrs:{link:t.preview.url,responsive:!0,text:t.$t("open"),icon:"open",target:"_blank"}}),n("k-dropdown",{staticClass:"k-file-view-options"},[n("k-button",{attrs:{disabled:t.isLocked,responsive:!0,text:t.$t("settings"),icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}}),n("k-dropdown-content",{ref:"settings",attrs:{options:t.$dropdown(t.id)},on:{action:t.action}})],1),n("k-languages-dropdown")],1)]},proxy:!0},{key:"right",fn:function(){return[n("k-prev-next",{attrs:{prev:t.prev,next:t.next}})]},proxy:!0}])},[t._v(" "+t._s(t.model.filename)+" ")]),n("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("file.blueprint",{blueprint:t.$esc(t.blueprint)}),lock:t.lock,parent:t.id,tab:t.tab}}),n("k-upload",{ref:"upload",on:{success:t.onUpload}})],1)],1)])}),[],!1,Tf,null,null,null);function Tf(t){for(let e in Of)this[e]=Of[e]}var Af=function(){return Ef.exports}();const Mf={props:{isInstallable:Boolean,isInstalled:Boolean,isOk:Boolean,requirements:Object,translations:Array},data(){return{user:{name:"",email:"",language:this.$translation.code,password:"",role:"admin"}}},computed:{fields(){return{email:{label:this.$t("email"),type:"email",link:!1,autofocus:!0,required:!0},password:{label:this.$t("password"),type:"password",placeholder:this.$t("password")+" …",required:!0},language:{label:this.$t("language"),type:"select",options:this.translations,icon:"globe",empty:!1,required:!0}}},isReady(){return this.isOk&&this.isInstallable},isComplete(){return this.isOk&&this.isInstalled}},methods:{async install(){try{await this.$api.system.install(this.user),await this.$reload({globals:["$system","$translation"]}),this.$store.dispatch("notification/success",this.$t("welcome")+"!")}catch(t){this.$store.dispatch("notification/error",t)}}}},If={};var Lf=Rt(Mf,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-panel",[n("k-view",{staticClass:"k-installation-view",attrs:{align:"center"}},[t.isComplete?n("k-text",[n("k-headline",[t._v(t._s(t.$t("installation.completed")))]),n("k-link",{attrs:{to:"/login"}},[t._v(" "+t._s(t.$t("login"))+" ")])],1):t.isReady?n("form",{on:{submit:function(e){return e.preventDefault(),t.install.apply(null,arguments)}}},[n("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("installation"))+" ")]),n("k-fieldset",{attrs:{fields:t.fields,novalidate:!0},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}}),n("k-button",{attrs:{text:t.$t("install"),type:"submit",icon:"check"}})],1):n("div",[n("k-headline",[t._v(" "+t._s(t.$t("installation.issues.headline"))+" ")]),n("ul",{staticClass:"k-installation-issues"},[!1===t.isInstallable?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.disabled"))}})],1):t._e(),!1===t.requirements.php?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.php"))}})],1):t._e(),!1===t.requirements.server?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.server"))}})],1):t._e(),!1===t.requirements.mbstring?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.mbstring"))}})],1):t._e(),!1===t.requirements.curl?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.curl"))}})],1):t._e(),!1===t.requirements.accounts?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.accounts"))}})],1):t._e(),!1===t.requirements.content?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.content"))}})],1):t._e(),!1===t.requirements.media?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.media"))}})],1):t._e(),!1===t.requirements.sessions?n("li",[n("k-icon",{attrs:{type:"alert"}}),n("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.sessions"))}})],1):t._e()]),n("k-button",{attrs:{text:t.$t("retry"),icon:"refresh"},on:{click:t.$reload}})],1)],1)],1)}),[],!1,jf,null,null,null);function jf(t){for(let e in If)this[e]=If[e]}var Df=function(){return Lf.exports}();const Bf={};var Pf=Rt({props:{languages:{type:Array,default:()=>[]}},computed:{languagesCollection(){return this.languages.map((t=>l(a({},t),{image:{back:"black",color:"gray",icon:"globe"},link:()=>{this.$dialog(`languages/${t.id}/update`)},options:[{icon:"edit",text:this.$t("edit"),click(){this.$dialog(`languages/${t.id}/update`)}},{icon:"trash",text:this.$t("delete"),disabled:t.default&&1!==this.languages.length,click(){this.$dialog(`languages/${t.id}/delete`)}}]})))},primaryLanguage(){return this.languagesCollection.filter((t=>t.default))},secondaryLanguages(){return this.languagesCollection.filter((t=>!1===t.default))}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",[n("k-view",{staticClass:"k-languages-view"},[n("k-header",[t._v(" "+t._s(t.$t("view.languages"))+" "),n("k-button-group",{attrs:{slot:"left"},slot:"left"},[n("k-button",{attrs:{text:t.$t("language.create"),icon:"add"},on:{click:function(e){return t.$dialog("languages/create")}}})],1)],1),n("section",{staticClass:"k-languages"},[t.languages.length>0?[n("section",{staticClass:"k-languages-view-section"},[n("header",{staticClass:"k-languages-view-section-header"},[n("k-headline",[t._v(t._s(t.$t("languages.default")))])],1),n("k-collection",{attrs:{items:t.primaryLanguage}})],1),n("section",{staticClass:"k-languages-view-section"},[n("header",{staticClass:"k-languages-view-section-header"},[n("k-headline",[t._v(t._s(t.$t("languages.secondary")))])],1),t.secondaryLanguages.length?n("k-collection",{attrs:{items:t.secondaryLanguages}}):n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$dialog("languages/create")}}},[t._v(" "+t._s(t.$t("languages.secondary.empty"))+" ")])],1)]:0===t.languages.length?[n("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$dialog("languages/create")}}},[t._v(" "+t._s(t.$t("languages.empty"))+" ")])]:t._e()],2)],1)],1)}),[],!1,Nf,null,null,null);function Nf(t){for(let e in Bf)this[e]=Bf[e]}var qf=function(){return Pf.exports}(),Rf=function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-panel",["login"===t.form?n("k-view",{staticClass:"k-login-view",attrs:{align:"center"}},[n("k-login-plugin",{attrs:{methods:t.methods}})],1):"code"===t.form?n("k-view",{staticClass:"k-login-code-view",attrs:{align:"center"}},[n("k-login-code",t._b({},"k-login-code",t.$props,!1))],1):t._e()],1)},Ff=[];const zf={components:{"k-login-plugin":window.panel.plugins.login||Un},props:{methods:Array,pending:Object},computed:{form(){return this.pending.email?"code":this.$user?null:"login"}},created(){this.$store.dispatch("content/clear")}},Yf={};var Hf=Rt(zf,Rf,Ff,!1,Uf,null,null,null);function Uf(t){for(let e in Yf)this[e]=Yf[e]}var Kf=function(){return Hf.exports}();const Jf={};var Gf=Rt({extends:ff,props:{status:Object}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[n("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[n("k-view",{staticClass:"k-page-view",attrs:{"data-locked":t.isLocked,"data-id":t.model.id,"data-template":t.blueprint}},[n("k-header",{attrs:{editable:t.permissions.changeTitle&&!t.isLocked,tab:t.tab.name,tabs:t.tabs},on:{edit:function(e){return t.$dialog(t.id+"/changeTitle")}},scopedSlots:t._u([{key:"left",fn:function(){return[n("k-button-group",[t.permissions.preview&&t.model.previewUrl?n("k-button",{staticClass:"k-page-view-preview",attrs:{link:t.model.previewUrl,responsive:!0,text:t.$t("open"),icon:"open",target:"_blank"}}):t._e(),t.status?n("k-status-icon",{attrs:{status:t.model.status,disabled:!t.permissions.changeStatus||t.isLocked,responsive:!0,text:t.status.label},on:{click:function(e){return t.$dialog(t.id+"/changeStatus")}}}):t._e(),n("k-dropdown",{staticClass:"k-page-view-options"},[n("k-button",{attrs:{disabled:!0===t.isLocked,responsive:!0,text:t.$t("settings"),icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}}),n("k-dropdown-content",{ref:"settings",attrs:{options:t.$dropdown(t.id)}})],1),n("k-languages-dropdown")],1)]},proxy:!0},{key:"right",fn:function(){return[t.model.id?n("k-prev-next",{attrs:{prev:t.prev,next:t.next}}):t._e()]},proxy:!0}])},[t._v(" "+t._s(t.model.title)+" ")]),n("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("page.blueprint",{blueprint:t.$esc(t.blueprint)}),lock:t.lock,parent:t.id,tab:t.tab}})],1)],1)}),[],!1,Vf,null,null,null);function Vf(t){for(let e in Jf)this[e]=Jf[e]}var Wf=function(){return Gf.exports}();const Xf={};var Zf=Rt({props:{id:String},computed:{view(){return"k-"+this.id+"-plugin-view"}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",[n(t.view,{tag:"component"})],1)}),[],!1,Qf,null,null,null);function Qf(t){for(let e in Xf)this[e]=Xf[e]}var tm=function(){return Zf.exports}();const em={};var nm=Rt({data:()=>({isLoading:!1,issue:"",values:{password:null,passwordConfirmation:null}}),computed:{fields(){return{password:{autofocus:!0,label:this.$t("user.changePassword.new"),icon:"key",type:"password"},passwordConfirmation:{label:this.$t("user.changePassword.new.confirm"),icon:"key",type:"password"}}}},mounted(){this.$store.dispatch("title",this.$t("view.resetPassword"))},methods:{async submit(){if(!this.values.password||this.values.password.length<8)return this.issue=this.$t("error.user.password.invalid"),!1;if(this.values.password!==this.values.passwordConfirmation)return this.issue=this.$t("error.user.password.notSame"),!1;this.isLoading=!0;try{await this.$api.users.changePassword(this.$user.id,this.values.password),this.$store.dispatch("notification/success",":)"),this.$go("/")}catch(t){this.issue=t.message}finally{this.isLoading=!1}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",[n("k-view",{staticClass:"k-password-reset-view",attrs:{align:"center"}},[n("k-form",{attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},scopedSlots:t._u([{key:"header",fn:function(){return[n("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("view.resetPassword"))+" ")]),t.issue?n("k-login-alert",{on:{click:function(e){t.issue=null}}},[t._v(" "+t._s(t.issue)+" ")]):t._e(),n("k-user-info",{attrs:{user:t.$user}})]},proxy:!0},{key:"footer",fn:function(){return[n("div",{staticClass:"k-login-buttons"},[n("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v(" "+t._s(t.$t("change"))+" "),t.isLoading?[t._v(" … ")]:t._e()],2)],1)]},proxy:!0}]),model:{value:t.values,callback:function(e){t.values=e},expression:"values"}})],1)],1)}),[],!1,sm,null,null,null);function sm(t){for(let e in em)this[e]=em[e]}var im=function(){return nm.exports}();const om={};var rm=Rt({extends:ff},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[n("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[n("k-view",{staticClass:"k-site-view",attrs:{"data-locked":t.isLocked,"data-id":"/","data-template":"site"}},[n("k-header",{attrs:{editable:t.permissions.changeTitle&&!t.isLocked,tabs:t.tabs,tab:t.tab.name},on:{edit:function(e){return t.$dialog("site/changeTitle")}},scopedSlots:t._u([{key:"left",fn:function(){return[n("k-button-group",[n("k-button",{staticClass:"k-site-view-preview",attrs:{link:t.model.previewUrl,responsive:!0,text:t.$t("open"),icon:"open",target:"_blank"}}),n("k-languages-dropdown")],1)]},proxy:!0}])},[t._v(" "+t._s(t.model.title)+" ")]),n("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("site.blueprint"),lock:t.lock,tab:t.tab,parent:"site"},on:{submit:function(e){return t.$emit("submit",e)}}})],1)],1)}),[],!1,am,null,null,null);function am(t){for(let e in om)this[e]=om[e]}var lm=function(){return rm.exports}();const um={props:{debug:Boolean,license:String,php:String,plugins:Array,server:String,https:Boolean,version:String}},cm={};var dm=Rt(um,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",[n("k-view",{staticClass:"k-system-view"},[n("k-header",[t._v(" "+t._s(t.$t("view.system"))+" ")]),n("section",{staticClass:"k-system-view-section"},[n("header",{staticClass:"k-system-view-section-header"},[n("k-headline",[t._v("Kirby")])],1),n("ul",{staticClass:"k-system-info-box",staticStyle:{"--columns":"2"}},[n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("license")))]),n("dd",[t.$license?[t._v(" "+t._s(t.license)+" ")]:n("k-button",{staticClass:"k-system-warning",on:{click:function(e){return t.$dialog("registration")}}},[t._v(" "+t._s(t.$t("license.unregistered"))+" ")])],2)])]),n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("version")))]),n("dd",{attrs:{dir:"ltr"}},[n("k-link",{attrs:{to:"https://github.com/getkirby/kirby/releases/tag/"+t.version}},[t._v(" "+t._s(t.version)+" ")])],1)])])])]),n("section",{staticClass:"k-system-view-section"},[n("header",{staticClass:"k-system-view-section-header"},[n("k-headline",[t._v(t._s(t.$t("environment")))])],1),n("ul",{staticClass:"k-system-info-box",staticStyle:{"--columns":"4"}},[n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("debugging")))]),n("dd",{class:{"k-system-warning":t.debug}},[t._v(" "+t._s(t.debug?t.$t("on"):t.$t("off"))+" ")])])]),n("li",[n("dl",[n("dt",[t._v("HTTPS")]),n("dd",{class:{"k-system-warning":!t.https}},[t._v(" "+t._s(t.https?t.$t("on"):t.$t("off"))+" ")])])]),n("li",[n("dl",[n("dt",[t._v("PHP")]),n("dd",[t._v(" "+t._s(t.php)+" ")])])]),n("li",[n("dl",[n("dt",[t._v(t._s(t.$t("server")))]),n("dd",[t._v(" "+t._s(t.server||"?")+" ")])])])])]),t.plugins.length?n("section",{staticClass:"k-system-view-section"},[n("header",{staticClass:"k-system-view-section-header"},[n("k-headline",{attrs:{link:"https://getkirby.com/plugins"}},[t._v(" "+t._s(t.$t("plugins"))+" ")])],1),n("table",{staticClass:"k-system-plugins"},[n("tr",[n("th",[t._v(" "+t._s(t.$t("name"))+" ")]),n("th",{staticClass:"desk"},[t._v(" "+t._s(t.$t("author"))+" ")]),n("th",{staticClass:"desk"},[t._v(" "+t._s(t.$t("license"))+" ")]),n("th",{staticStyle:{width:"8rem"}},[t._v(" "+t._s(t.$t("version"))+" ")])]),t._l(t.plugins,(function(e){return n("tr",{key:e.name},[n("td",[e.link?n("k-link",{attrs:{to:e.link}},[t._v(" "+t._s(e.name)+" ")]):[t._v(" "+t._s(e.name)+" ")]],2),n("td",{staticClass:"desk"},[t._v(" "+t._s(e.author||"-")+" ")]),n("td",{staticClass:"desk"},[t._v(" "+t._s(e.license||"-")+" ")]),n("td",{staticStyle:{width:"8rem"}},[t._v(" "+t._s(e.version||"-")+" ")])])}))],2)]):t._e()],1)],1)}),[],!1,pm,null,null,null);function pm(t){for(let e in cm)this[e]=cm[e]}var hm=function(){return dm.exports}();const fm={};var mm=Rt({props:{role:Object,roles:Array,search:String,title:String,users:Object},computed:{items(){return this.users.data.map((t=>(t.options=this.$dropdown(t.link),t)))}},methods:{paginate(t){this.$reload({query:{page:t.page}})}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-inside",[n("k-view",{staticClass:"k-users-view"},[n("k-header",{scopedSlots:t._u([{key:"left",fn:function(){return[n("k-button-group",{attrs:{buttons:[{disabled:!1===t.$permissions.users.create,text:t.$t("user.create"),icon:"add",click:function(){return t.$dialog("users/create")}}]}})]},proxy:!0},{key:"right",fn:function(){return[n("k-button-group",[n("k-dropdown",[n("k-button",{attrs:{responsive:!0,text:t.$t("role")+": "+(t.role?t.role.title:t.$t("role.all")),icon:"funnel"},on:{click:function(e){return t.$refs.roles.toggle()}}}),n("k-dropdown-content",{ref:"roles",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{icon:"bolt",link:"/users"}},[t._v(" "+t._s(t.$t("role.all"))+" ")]),n("hr"),t._l(t.roles,(function(e){return n("k-dropdown-item",{key:e.id,attrs:{link:"/users/?role="+e.id,icon:"bolt"}},[t._v(" "+t._s(e.title)+" ")])}))],2)],1)],1)]},proxy:!0}])},[t._v(" "+t._s(t.$t("view.users"))+" ")]),t.users.data.length>0?[n("k-collection",{attrs:{items:t.items,pagination:t.users.pagination},on:{paginate:t.paginate}})]:0===t.users.pagination.total?[n("k-empty",{attrs:{icon:"users"}},[t._v(" "+t._s(t.$t("role.empty"))+" ")])]:t._e()],2)],1)}),[],!1,gm,null,null,null);function gm(t){for(let e in fm)this[e]=fm[e]}var km=function(){return mm.exports}();const vm={};var bm=Rt({computed:{placeholder(){return this.field("code",{}).placeholder},languages(){return this.field("language",{options:[]}).options}},methods:{focus(){this.$refs.code.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-block-type-code-editor"},[n("k-input",{ref:"code",attrs:{buttons:!1,placeholder:t.placeholder,spellcheck:!1,value:t.content.code,type:"textarea"},on:{input:function(e){return t.update({code:e})}}}),t.languages.length?n("div",{staticClass:"k-block-type-code-editor-language"},[n("k-icon",{attrs:{type:"code"}}),n("k-input",{ref:"language",attrs:{empty:!1,options:t.languages,value:t.content.language,type:"select"},on:{input:function(e){return t.update({language:e})}}})],1):t._e()],1)}),[],!1,ym,null,null,null);function ym(t){for(let e in vm)this[e]=vm[e]}var $m=function(){return bm.exports}(),_m=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:$m});const wm={};var xm=Rt({},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-block-title",{attrs:{content:t.content,fieldset:t.fieldset},on:{dblclick:function(e){return t.$emit("open")}}})}),[],!1,Sm,null,null,null);function Sm(t){for(let e in wm)this[e]=wm[e]}var Cm=function(){return xm.exports}(),Om=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:Cm});const Em={};var Tm=Rt({},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("ul",{on:{dblclick:t.open}},[0===t.content.images.length?[n("li"),n("li"),n("li"),n("li"),n("li")]:t._l(t.content.images,(function(t){return n("li",{key:t.id},[n("img",{attrs:{src:t.url,srcset:t.image.srcset,alt:t.alt}})])}))],2)}),[],!1,Am,null,null,null);function Am(t){for(let e in Em)this[e]=Em[e]}var Mm=function(){return Tm.exports}(),Im=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:Mm});const Lm={};var jm=Rt({computed:{textField(){return this.field("text",{marks:!0})}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-block-type-heading-input",attrs:{"data-level":t.content.level}},[n("k-writer",{ref:"input",attrs:{inline:!0,marks:t.textField.marks,placeholder:t.textField.placeholder,value:t.content.text},on:{input:function(e){return t.update({text:e})}}})],1)}),[],!1,Dm,null,null,null);function Dm(t){for(let e in Lm)this[e]=Lm[e]}var Bm=function(){return jm.exports}(),Pm=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:Bm});const Nm={};var qm=Rt({computed:{captionMarks(){return this.field("caption",{marks:!0}).marks},crop(){return this.content.crop||!1},src(){var t;return"web"===this.content.location?this.content.src:!!(null==(t=this.content.image[0])?void 0:t.url)&&this.content.image[0].url},ratio(){return this.content.ratio||!1}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-block-figure",{attrs:{caption:t.content.caption,"caption-marks":t.captionMarks,"empty-text":t.$t("field.blocks.image.placeholder")+" …","is-empty":!t.src,"empty-icon":"image"},on:{open:t.open,update:t.update}},[t.src?[t.ratio?n("k-aspect-ratio",{attrs:{ratio:t.ratio,cover:t.crop}},[n("img",{attrs:{alt:t.content.alt,src:t.src}})]):n("img",{staticClass:"k-block-type-image-auto",attrs:{alt:t.content.alt,src:t.src}})]:t._e()],2)}),[],!1,Rm,null,null,null);function Rm(t){for(let e in Nm)this[e]=Nm[e]}var Fm=function(){return qm.exports}(),zm=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:Fm});const Ym={};var Hm=Rt({},(function(){var t=this;t.$createElement;return t._self._c,t._m(0)}),[function(){var t=this.$createElement,e=this._self._c||t;return e("div",[e("hr")])}],!1,Um,null,null,null);function Um(t){for(let e in Ym)this[e]=Ym[e]}var Km=function(){return Hm.exports}(),Jm=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:Km});const Gm={};var Vm=Rt({computed:{marks(){return this.field("text",{}).marks}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-input",{ref:"input",staticClass:"k-block-type-list-input",attrs:{marks:t.marks,value:t.content.text,type:"list"},on:{input:function(e){return t.update({text:e})}}})}),[],!1,Wm,null,null,null);function Wm(t){for(let e in Gm)this[e]=Gm[e]}var Xm=function(){return Vm.exports}(),Zm=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:Xm});const Qm={};var tg=Rt({computed:{placeholder(){return this.field("text",{}).placeholder}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-input",{ref:"input",staticClass:"k-block-type-markdown-input",attrs:{buttons:!1,placeholder:t.placeholder,spellcheck:!1,value:t.content.text,type:"textarea"},on:{input:function(e){return t.update({text:e})}}})}),[],!1,eg,null,null,null);function eg(t){for(let e in Qm)this[e]=Qm[e]}var ng=function(){return tg.exports}(),sg=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:ng});const ig={};var og=Rt({computed:{citationField(){return this.field("citation",{})},textField(){return this.field("text",{})}},methods:{focus(){this.$refs.text.focus()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{staticClass:"k-block-type-quote-editor"},[n("k-writer",{ref:"text",staticClass:"k-block-type-quote-text",attrs:{inline:!0,marks:t.textField.marks,placeholder:t.textField.placeholder,value:t.content.text},on:{input:function(e){return t.update({text:e})}}}),n("k-writer",{ref:"citation",staticClass:"k-block-type-quote-citation",attrs:{inline:!0,marks:t.citationField.marks,placeholder:t.citationField.placeholder,value:t.content.citation},on:{input:function(e){return t.update({citation:e})}}})],1)}),[],!1,rg,null,null,null);function rg(t){for(let e in ig)this[e]=ig[e]}var ag=function(){return og.exports}(),lg=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:ag});const ug={};var cg=Rt({mixins:[Da],inheritAttrs:!1,computed:{columns(){return this.table.columns||this.fields},columnsCount(){return Object.keys(this.columns).length},fields(){return this.table.fields||{}},rows(){return this.content.rows||[]},table(){let t=null;return Object.values(this.fieldset.tabs).forEach((e=>{e.fields.rows&&(t=e.fields.rows)})),t||{}}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("table",{staticClass:"k-block-type-table-preview",on:{dblclick:t.open}},[n("tr",t._l(t.columns,(function(e,s){return n("th",{key:s,style:"width:"+t.width(e.width),attrs:{"data-align":e.align}},[t._v(" "+t._s(e.label)+" ")])})),0),0===t.rows.length?n("tr",[n("td",{attrs:{colspan:t.columnsCount}},[n("small",{staticClass:"k-block-type-table-preview-empty"},[t._v(t._s(t.$t("field.structure.empty")))])])]):t._l(t.rows,(function(e,s){return n("tr",{key:s},t._l(t.columns,(function(i,o){return n("td",{key:s+"-"+o,style:"width:"+t.width(i.width),attrs:{"data-align":i.align}},[!1===t.columnIsEmpty(e[o])?[t.previewExists(i.type)?n("k-"+i.type+"-field-preview",{tag:"component",attrs:{value:e[o],column:i,field:t.fields[o]}}):[n("p",{staticClass:"k-structure-table-text"},[t._v(" "+t._s(i.before)+" "+t._s(t.displayText(t.fields[o],e[o])||"–")+" "+t._s(i.after)+" ")])]]:t._e()],2)})),0)}))],2)}),[],!1,dg,null,null,null);function dg(t){for(let e in ug)this[e]=ug[e]}var pg=function(){return cg.exports}(),hg=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:pg});const fg={};var mg=Rt({props:{endpoints:Object},computed:{textField(){return this.field("text",{})}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t.$createElement;return(t._self._c||e)("k-writer",{ref:"input",staticClass:"k-block-type-text-input",attrs:{inline:t.textField.inline,marks:t.textField.marks,nodes:t.textField.nodes,placeholder:t.textField.placeholder,value:t.content.text},on:{input:function(e){return t.update({text:e})}}})}),[],!1,gg,null,null,null);function gg(t){for(let e in fg)this[e]=fg[e]}var kg=function(){return mg.exports}(),vg=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:kg});const bg={};var yg=Rt({computed:{captionMarks(){return this.field("caption",{marks:!0}).marks},video(){return this.$helper.embed.video(this.content.url,!0)}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-block-figure",{attrs:{caption:t.content.caption,"caption-marks":t.captionMarks,"empty-text":t.$t("field.blocks.video.placeholder")+" …","is-empty":!t.video,"empty-icon":"video"},on:{open:t.open,update:t.update}},[n("k-aspect-ratio",{attrs:{ratio:"16/9"}},[t.video?n("iframe",{attrs:{src:t.video,referrerpolicy:"strict-origin-when-cross-origin"}}):t._e()])],1)}),[],!1,$g,null,null,null);function $g(t){for(let e in bg)this[e]=bg[e]}var _g=function(){return yg.exports}(),wg=Object.freeze({__proto__:null,[Symbol.toStringTag]:"Module",default:_g});const xg={inheritAttrs:!1,props:{attrs:[Array,Object],content:[Array,Object],endpoints:Object,fieldset:Object,id:String,isBatched:Boolean,isFull:Boolean,isHidden:Boolean,isLastInBatch:Boolean,isSelected:Boolean,name:String,next:Object,prev:Object,type:String},data:()=>({skipFocus:!1}),computed:{className(){let t=["k-block-type-"+this.type];return this.fieldset.preview!==this.type&&t.push("k-block-type-"+this.fieldset.preview),!1===this.wysiwyg&&t.push("k-block-type-default"),t},customComponent(){return this.wysiwyg?this.wysiwygComponent:"k-block-type-default"},isEditable(){return!1!==this.fieldset.editable},listeners(){return l(a({},this.$listeners),{confirmToRemove:this.confirmToRemove,open:this.open})},tabs(){let t=this.fieldset.tabs;return Object.entries(t).forEach((([e,n])=>{Object.entries(n.fields).forEach((([n])=>{t[e].fields[n].section=this.name,t[e].fields[n].endpoints={field:this.endpoints.field+"/fieldsets/"+this.type+"/fields/"+n,section:this.endpoints.section,model:this.endpoints.model}}))})),t},wysiwyg(){return!1!==this.wysiwygComponent},wysiwygComponent(){if(!1===this.fieldset.preview)return!1;let t;return this.fieldset.preview&&(t="k-block-type-"+this.fieldset.preview,this.$helper.isComponent(t))?t:(t="k-block-type-"+this.type,!!this.$helper.isComponent(t)&&t)}},methods:{close(){this.$refs.drawer.close()},confirmToRemove(){this.$refs.removeDialog.open()},focus(){!0!==this.skipFocus&&("function"==typeof this.$refs.editor.focus?this.$refs.editor.focus():this.$refs.container.focus())},onFocusIn(t){var e,n;(null==(n=null==(e=this.$refs.options)?void 0:e.$el)?void 0:n.contains(t.target))||this.$emit("focus",t)},goTo(t){t&&(this.skipFocus=!0,this.close(),this.$nextTick((()=>{t.$refs.container.focus(),t.open(),this.skipFocus=!1})))},open(){this.$refs.drawer&&this.$refs.drawer.open()},remove(){this.$refs.removeDialog.close(),this.$emit("remove",this.id)}}},Sg={};var Cg=Rt(xg,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{ref:"container",staticClass:"k-block-container",class:"k-block-container-type-"+t.type,attrs:{"data-batched":t.isBatched,"data-disabled":t.fieldset.disabled,"data-hidden":t.isHidden,"data-id":t.id,"data-last-in-batch":t.isLastInBatch,"data-selected":t.isSelected,"data-translate":t.fieldset.translate,tabindex:"0"},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:e.ctrlKey&&e.shiftKey?(e.preventDefault(),t.$emit("sortDown")):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:e.ctrlKey&&e.shiftKey?(e.preventDefault(),t.$emit("sortUp")):null}],focus:function(e){return t.$emit("focus")},focusin:t.onFocusIn}},[n("div",{staticClass:"k-block",class:t.className},[n(t.customComponent,t._g(t._b({ref:"editor",tag:"component"},"component",t.$props,!1),t.listeners))],1),n("k-block-options",t._g({ref:"options",attrs:{"is-batched":t.isBatched,"is-editable":t.isEditable,"is-full":t.isFull,"is-hidden":t.isHidden}},t.listeners)),t.isEditable&&!t.isBatched?n("k-form-drawer",{ref:"drawer",staticClass:"k-block-drawer",attrs:{id:t.id,icon:t.fieldset.icon||"box",tabs:t.tabs,title:t.fieldset.name,value:t.content},on:{close:function(e){return t.focus()},input:function(e){return t.$emit("update",e)}},scopedSlots:t._u([{key:"options",fn:function(){return[t.isHidden?n("k-button",{staticClass:"k-drawer-option",attrs:{icon:"hidden"},on:{click:function(e){return t.$emit("show")}}}):t._e(),n("k-button",{staticClass:"k-drawer-option",attrs:{disabled:!t.prev,icon:"angle-left"},on:{click:function(e){return e.preventDefault(),e.stopPropagation(),t.goTo(t.prev)}}}),n("k-button",{staticClass:"k-drawer-option",attrs:{disabled:!t.next,icon:"angle-right"},on:{click:function(e){return e.preventDefault(),e.stopPropagation(),t.goTo(t.next)}}}),n("k-button",{staticClass:"k-drawer-option",attrs:{icon:"trash"},on:{click:function(e){return e.preventDefault(),e.stopPropagation(),t.confirmToRemove.apply(null,arguments)}}})]},proxy:!0}],null,!1,2211169536)}):t._e(),n("k-remove-dialog",{ref:"removeDialog",attrs:{text:t.$t("field.blocks.delete.confirm")},on:{submit:t.remove}})],1)}),[],!1,Og,null,null,null);function Og(t){for(let e in Sg)this[e]=Sg[e]}var Eg=function(){return Cg.exports}();const Tg={};var Ag=Rt({inheritAttrs:!1,computed:{shortcut(){return this.$helper.keyboard.metaKey()+"+v"}},methods:{close(){this.$refs.dialog.close()},open(){this.$refs.dialog.open()},onPaste(t){this.$emit("paste",t),this.close()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-block-importer",attrs:{"cancel-button":!1,"submit-button":!1,size:"large"}},[n("label",{attrs:{for:"pasteboard"},domProps:{innerHTML:t._s(t.$t("field.blocks.fieldsets.paste",{shortcut:t.shortcut}))}}),n("textarea",{attrs:{id:"pasteboard"},on:{paste:function(e){return e.preventDefault(),t.onPaste.apply(null,arguments)}}})])}),[],!1,Mg,null,null,null);function Mg(t){for(let e in Tg)this[e]=Tg[e]}const Ig={components:{"k-block-pasteboard":function(){return Ag.exports}()},inheritAttrs:!1,props:{autofocus:Boolean,empty:String,endpoints:Object,fieldsets:Object,fieldsetGroups:Object,group:String,max:{type:Number,default:null},value:{type:Array,default:()=>[]}},data(){return{isMultiSelectKey:!1,batch:[],blocks:this.value,current:null,isFocussed:!1}},computed:{draggableOptions(){return{id:this._uid,handle:".k-sort-handle",list:this.blocks,move:this.move,delay:10,data:{fieldsets:this.fieldsets,isFull:this.isFull},options:{group:this.group}}},hasFieldsets(){return Object.keys(this.fieldsets).length},isEditing(){return this.$store.state.dialog||this.$store.state.drawers.open.length>0},isEmpty(){return 0===this.blocks.length},isFull(){return null!==this.max&&this.blocks.length>=this.max},selected(){return this.current},selectedOrBatched(){return this.batch.length>0?this.batch:this.selected?[this.selected]:[]}},watch:{value(){this.blocks=this.value}},created(){this.$events.$on("copy",this.copy),this.$events.$on("focus",this.onOutsideFocus),this.$events.$on("keydown",this.onKey),this.$events.$on("keyup",this.onKey),this.$events.$on("paste",this.onPaste)},destroyed(){this.$events.$off("copy",this.copy),this.$events.$off("focus",this.onOutsideFocus),this.$events.$off("keydown",this.onKey),this.$events.$off("keyup",this.onKey),this.$events.$off("paste",this.onPaste)},mounted(){!0===this.$props.autofocus&&this.focus()},methods:{append(t,e){if("string"!=typeof t){if(Array.isArray(t)){let n=this.$helper.clone(t).map((t=>(t.id=this.$helper.uuid(),t)));const s=Object.keys(this.fieldsets);if(n=n.filter((t=>s.includes(t.type))),this.max){const t=this.max-this.blocks.length;n=n.slice(0,t)}this.blocks.splice(e,0,...n),this.save()}}else this.add(t,e)},async add(t="text",e){const n=await this.$api.get(this.endpoints.field+"/fieldsets/"+t);this.blocks.splice(e,0,n),this.save(),this.$nextTick((()=>{this.focusOrOpen(n)}))},addToBatch(t){null!==this.selected&&!1===this.batch.includes(this.selected)&&(this.batch.push(this.selected),this.current=null),!1===this.batch.includes(t.id)&&this.batch.push(t.id)},choose(t){if(1===Object.keys(this.fieldsets).length){const e=Object.values(this.fieldsets)[0].type;this.add(e,t)}else this.$refs.selector.open(t)},chooseToConvert(t){this.$refs.selector.open(t,{disabled:[t.type],headline:this.$t("field.blocks.changeType"),event:"convert"})},click(t){this.$emit("click",t)},confirmToRemoveAll(){this.$refs.removeAll.open()},confirmToRemoveSelected(){this.$refs.removeSelected.open()},copy(t){if(!0===this.isEditing)return!1;if(0===this.blocks.length)return!1;if(0===this.selectedOrBatched.length)return!1;if(!0===this.isInputEvent(t))return!1;let e=[];if(this.blocks.forEach((t=>{this.selectedOrBatched.includes(t.id)&&e.push(t)})),0===e.length)return!1;this.$helper.clipboard.write(e,t),t instanceof ClipboardEvent==!1&&(this.batch=this.selectedOrBatched),this.$store.dispatch("notification/success",`${e.length} copied!`)},copyAll(){this.selectAll(),this.copy(),this.deselectAll()},async convert(t,e){var n;const s=this.findIndex(e.id);if(-1===s)return!1;const i=t=>{var e;let n={};for(const s of Object.values(null!=(e=null==t?void 0:t.tabs)?e:{}))n=a(a({},n),s.fields);return n},o=this.blocks[s],r=await this.$api.get(this.endpoints.field+"/fieldsets/"+t),u=this.fieldsets[o.type],c=this.fieldsets[t];if(!c)return!1;let d=r.content;const p=i(c),h=i(u);for(const[a,l]of Object.entries(p)){const t=h[a];(null==t?void 0:t.type)===l.type&&(null==(n=null==o?void 0:o.content)?void 0:n[a])&&(d[a]=o.content[a])}this.blocks[s]=l(a({},r),{id:o.id,content:d}),this.save()},deselectAll(){this.batch=[],this.current=null},async duplicate(t,e){const n=l(a({},this.$helper.clone(t)),{id:this.$helper.uuid()});this.blocks.splice(e+1,0,n),this.save()},fieldset(t){return this.fieldsets[t.type]||{icon:"box",name:t.type,tabs:{content:{fields:{}}},type:t.type}},find(t){return this.blocks.find((e=>e.id===t))},findIndex(t){return this.blocks.findIndex((e=>e.id===t))},focus(t){(null==t?void 0:t.id)&&this.$refs["block-"+t.id]?this.$refs["block-"+t.id][0].focus():this.blocks[0]&&this.focus(this.blocks[0])},focusOrOpen(t){this.fieldsets[t.type].wysiwyg?this.focus(t):this.open(t)},hide(t){this.$set(t,"isHidden",!0),this.save()},isBatched(t){return this.batch.includes(t.id)},isInputEvent(){const t=document.querySelector(":focus");return t&&t.matches("input, textarea, [contenteditable], .k-writer")},isLastInBatch(t){const[e]=this.batch.slice(-1);return e&&t.id===e},isOnlyInstance:()=>1===document.querySelectorAll(".k-blocks").length,isSelected(t){return this.selected&&this.selected===t.id},move(t){if(t.from!==t.to){const e=t.draggedContext.element,n=t.relatedContext.component.componentData||t.relatedContext.component.$parent.componentData;if(!1===Object.keys(n.fieldsets).includes(e.type))return!1;if(!0===n.isFull)return!1}return!0},onKey(t){this.isMultiSelectKey=t.metaKey||t.ctrlKey||t.altKey},onOutsideFocus(t){if(t.target.closest(".k-dialog"))return;const e=document.querySelector(".k-overlay:last-of-type");if(!1===this.$el.contains(t.target)&&(!e||!1===e.contains(t.target)))return this.select(null);if(e){const e=this.$el.closest(".k-layout-column");if(!1===(null==e?void 0:e.contains(t.target)))return this.select(null)}},onPaste(t){var e;return!0!==this.isInputEvent(t)&&(!0===this.isEditing?!0===(null==(e=this.$refs.selector)?void 0:e.isOpen())&&this.paste(t):(0!==this.selectedOrBatched.length||!0===this.isOnlyInstance())&&this.paste(t))},open(t){this.$refs["block-"+t.id]&&this.$refs["block-"+t.id][0].open()},async paste(t){const e=this.$helper.clipboard.read(t),n=await this.$api.post(this.endpoints.field+"/paste",{html:e});let s=this.selectedOrBatched[this.selectedOrBatched.length-1],i=this.findIndex(s);-1===i&&(i=this.blocks.length),this.append(n,i+1)},pasteboard(){this.$refs.pasteboard.open()},prevNext(t){if(this.blocks[t]){let e=this.blocks[t];if(this.$refs["block-"+e.id])return this.$refs["block-"+e.id][0]}},remove(t){var e;const n=this.findIndex(t.id);-1!==n&&((null==(e=this.selected)?void 0:e.id)===t.id&&this.select(null),this.$delete(this.blocks,n),this.save())},removeAll(){this.batch=[],this.blocks=[],this.save(),this.$refs.removeAll.close()},removeSelected(){this.batch.forEach((t=>{const e=this.findIndex(t);-1!==e&&this.$delete(this.blocks,e)})),this.deselectAll(),this.save(),this.$refs.removeSelected.close()},save(){this.$emit("input",this.blocks)},select(t,e=null){if(e&&this.isMultiSelectKey&&this.onKey(e),t&&this.isMultiSelectKey)return this.addToBatch(t),void(this.current=null);this.batch=[],this.current=t?t.id:null},selectAll(){this.batch=Object.values(this.blocks).map((t=>t.id))},show(t){this.$set(t,"isHidden",!1),this.save()},sort(t,e,n){if(n<0)return;let s=this.$helper.clone(this.blocks);s.splice(e,1),s.splice(n,0,t),this.blocks=s,this.save(),this.$nextTick((()=>{this.focus(t)}))},update(t,e){const n=this.findIndex(t.id);-1!==n&&Object.entries(e).forEach((([t,e])=>{this.$set(this.blocks[n].content,t,e)})),this.save()}}},Lg={};var jg=Rt(Ig,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",{ref:"wrapper",staticClass:"k-blocks",attrs:{"data-empty":0===t.blocks.length,"data-multi-select-key":t.isMultiSelectKey},on:{focusin:function(e){t.focussed=!0},focusout:function(e){t.focussed=!1}}},[t.hasFieldsets?[n("k-draggable",t._b({staticClass:"k-blocks-list",on:{sort:t.save},scopedSlots:t._u([{key:"footer",fn:function(){return[n("k-empty",{staticClass:"k-blocks-empty",attrs:{icon:"box"},on:{click:function(e){return t.choose(t.blocks.length)}}},[t._v(" "+t._s(t.empty||t.$t("field.blocks.empty"))+" ")])]},proxy:!0}],null,!1,2413899928)},"k-draggable",t.draggableOptions,!1),t._l(t.blocks,(function(e,s){return n("k-block",t._b({key:e.id,ref:"block-"+e.id,refInFor:!0,attrs:{endpoints:t.endpoints,fieldset:t.fieldset(e),"is-batched":t.isBatched(e),"is-last-in-batch":t.isLastInBatch(e),"is-full":t.isFull,"is-hidden":!0===e.isHidden,"is-selected":t.isSelected(e),next:t.prevNext(s+1),prev:t.prevNext(s-1)},on:{append:function(e){return t.append(e,s+1)},blur:function(e){return t.select(null)},choose:function(e){return t.choose(e)},chooseToAppend:function(e){return t.choose(s+1)},chooseToConvert:function(n){return t.chooseToConvert(e)},chooseToPrepend:function(e){return t.choose(s)},copy:function(e){return t.copy()},confirmToRemoveSelected:t.confirmToRemoveSelected,duplicate:function(n){return t.duplicate(e,s)},focus:function(n){return t.select(e)},hide:function(n){return t.hide(e)},paste:function(e){return t.pasteboard()},prepend:function(e){return t.add(e,s)},remove:function(n){return t.remove(e)},sortDown:function(n){return t.sort(e,s,s+1)},sortUp:function(n){return t.sort(e,s,s-1)},show:function(n){return t.show(e)},update:function(n){return t.update(e,n)}},nativeOn:{click:function(n){return n.stopPropagation(),t.select(e,n)}}},"k-block",e,!1))})),1),n("k-block-selector",{ref:"selector",attrs:{fieldsets:t.fieldsets,"fieldset-groups":t.fieldsetGroups},on:{add:t.add,convert:t.convert,paste:function(e){return t.paste(e)}}}),n("k-remove-dialog",{ref:"removeAll",attrs:{text:t.$t("field.blocks.delete.confirm.all")},on:{submit:t.removeAll}}),n("k-remove-dialog",{ref:"removeSelected",attrs:{text:t.$t("field.blocks.delete.confirm.selected")},on:{submit:t.removeSelected}}),n("k-block-pasteboard",{ref:"pasteboard",on:{paste:function(e){return t.paste(e)}}})]:[n("k-box",{attrs:{theme:"info"}},[t._v(" No fieldsets yet ")])]],2)}),[],!1,Dg,null,null,null);function Dg(t){for(let e in Lg)this[e]=Lg[e]}var Bg=function(){return jg.exports}();const Pg={inheritAttrs:!1,props:{caption:String,captionMarks:[Boolean,Array],cover:{type:Boolean,default:!0},isEmpty:Boolean,emptyIcon:String,emptyText:String,ratio:String},computed:{ratioPadding(){return this.$helper.ratio(this.ratio||"16/9")}}},Ng={};var qg=Rt(Pg,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("figure",{staticClass:"k-block-figure"},[t.isEmpty?n("k-button",{staticClass:"k-block-figure-empty",attrs:{icon:t.emptyIcon,text:t.emptyText},on:{click:function(e){return t.$emit("open")}}}):n("span",{staticClass:"k-block-figure-container",on:{dblclick:function(e){return t.$emit("open")}}},[t._t("default")],2),t.caption?n("figcaption",[n("k-writer",{attrs:{inline:!0,marks:t.captionMarks,value:t.caption},on:{input:function(e){return t.$emit("update",{caption:e})}}})],1):t._e()],1)}),[],!1,Rg,null,null,null);function Rg(t){for(let e in Ng)this[e]=Ng[e]}var Fg=function(){return qg.exports}();const zg={props:{isBatched:Boolean,isEditable:Boolean,isFull:Boolean,isHidden:Boolean},methods:{open(){this.$refs.options.open()}}},Yg={};var Hg=Rt(zg,(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dropdown",{staticClass:"k-block-options"},[t.isBatched?[n("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("copy"),icon:"template"},on:{click:function(e){return e.preventDefault(),t.$emit("copy")}}}),n("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("remove"),icon:"trash"},on:{click:function(e){return e.preventDefault(),t.$emit("confirmToRemoveSelected")}}})]:[t.isEditable?n("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("edit"),icon:"edit"},on:{click:function(e){return t.$emit("open")}}}):t._e(),n("k-button",{staticClass:"k-block-options-button",attrs:{disabled:t.isFull,tooltip:t.$t("insert.after"),icon:"add"},on:{click:function(e){return t.$emit("chooseToAppend")}}}),n("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("delete"),icon:"trash"},on:{click:function(e){return t.$emit("confirmToRemove")}}}),n("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("more"),icon:"dots"},on:{click:function(e){return t.$refs.options.toggle()}}}),n("k-button",{staticClass:"k-block-options-button k-sort-handle",attrs:{tooltip:t.$t("sort"),icon:"sort"},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.preventDefault(),t.$emit("sortUp"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.preventDefault(),t.$emit("sortDown"))}]}}),n("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"angle-up"},on:{click:function(e){return t.$emit("chooseToPrepend")}}},[t._v(" "+t._s(t.$t("insert.before"))+" ")]),n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"angle-down"},on:{click:function(e){return t.$emit("chooseToAppend")}}},[t._v(" "+t._s(t.$t("insert.after"))+" ")]),n("hr"),t.isEditable?n("k-dropdown-item",{attrs:{icon:"edit"},on:{click:function(e){return t.$emit("open")}}},[t._v(" "+t._s(t.$t("edit"))+" ")]):t._e(),n("k-dropdown-item",{attrs:{icon:"refresh"},on:{click:function(e){return t.$emit("chooseToConvert")}}},[t._v(" "+t._s(t.$t("field.blocks.changeType"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{icon:"template"},on:{click:function(e){return t.$emit("copy")}}},[t._v(" "+t._s(t.$t("copy"))+" ")]),n("k-dropdown-item",{attrs:{icon:"download"},on:{click:function(e){return t.$emit("paste")}}},[t._v(" "+t._s(t.$t("paste.after"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{icon:t.isHidden?"preview":"hidden"},on:{click:function(e){return t.$emit(t.isHidden?"show":"hide")}}},[t._v(" "+t._s(!0===t.isHidden?t.$t("show"):t.$t("hide"))+" ")]),n("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"copy"},on:{click:function(e){return t.$emit("duplicate")}}},[t._v(" "+t._s(t.$t("duplicate"))+" ")]),n("hr"),n("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){return t.$emit("confirmToRemove")}}},[t._v(" "+t._s(t.$t("delete"))+" ")])],1)]],2)}),[],!1,Ug,null,null,null);function Ug(t){for(let e in Yg)this[e]=Yg[e]}var Kg=function(){return Hg.exports}();const Jg={};var Gg=Rt({inheritAttrs:!1,props:{endpoint:String,fieldsets:Object,fieldsetGroups:Object},data(){return{dialogIsOpen:!1,disabled:[],headline:null,payload:null,event:"add",groups:this.createGroups()}},computed:{shortcut(){return this.$helper.keyboard.metaKey()+"+v"}},methods:{add(t){this.$emit(this.event,t,this.payload),this.$refs.dialog.close()},close(){this.$refs.dialog.close()},createGroups(){let t={},e=0;const n=this.fieldsetGroups||{blocks:{label:this.$t("field.blocks.fieldsets.label"),sets:Object.keys(this.fieldsets)}};return Object.keys(n).forEach((s=>{let i=n[s];i.open=!1!==i.open,i.fieldsets=i.sets.filter((t=>this.fieldsets[t])).map((t=>(e++,l(a({},this.fieldsets[t]),{index:e})))),0!==i.fieldsets.length&&(t[s]=i)})),t},isOpen(){return this.dialogIsOpen},navigate(t){var e,n;null==(n=null==(e=this.$refs["fieldset-"+t])?void 0:e[0])||n.focus()},onClose(){this.dialogIsOpen=!1,this.$events.$off("paste",this.close)},onOpen(){this.dialogIsOpen=!0,this.$events.$on("paste",this.close)},open(t,e={}){const n=a({event:"add",disabled:[],headline:null},e);this.event=n.event,this.disabled=n.disabled,this.headline=n.headline,this.payload=t,this.$refs.dialog.open()}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("k-dialog",{ref:"dialog",staticClass:"k-block-selector",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"},on:{open:t.onOpen,close:t.onClose}},[t.headline?n("k-headline",[t._v(" "+t._s(t.headline)+" ")]):t._e(),t._l(t.groups,(function(e,s){return n("details",{key:s,attrs:{open:e.open}},[n("summary",[t._v(t._s(e.label))]),n("div",{staticClass:"k-block-types"},t._l(e.fieldsets,(function(e){return n("k-button",{key:e.name,ref:"fieldset-"+e.index,refInFor:!0,attrs:{disabled:t.disabled.includes(e.type),icon:e.icon||"box",text:e.name},on:{keydown:[function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"up",38,n.key,["Up","ArrowUp"])?null:t.navigate(e.index-1)},function(n){return!n.type.indexOf("key")&&t._k(n.keyCode,"down",40,n.key,["Down","ArrowDown"])?null:t.navigate(e.index+1)}],click:function(n){return t.add(e.type)}}})})),1)])})),n("p",{staticClass:"k-clipboard-hint",domProps:{innerHTML:t._s(t.$t("field.blocks.fieldsets.paste",{shortcut:t.shortcut}))}})],2)}),[],!1,Vg,null,null,null);function Vg(t){for(let e in Jg)this[e]=Jg[e]}var Wg=function(){return Gg.exports}();const Xg={};var Zg=Rt({inheritAttrs:!1,props:{fieldset:Object,content:Object},computed:{icon(){return this.fieldset.icon||"box"},label(){if(!this.fieldset.label||0===this.fieldset.label.length)return!1;if(this.fieldset.label===this.fieldset.name)return!1;const t=this.$helper.string.template(this.fieldset.label,this.content);return"…"!==t&&t},name(){return this.fieldset.name}}},(function(){var t=this,e=t.$createElement,n=t._self._c||e;return n("div",t._g({staticClass:"k-block-title"},t.$listeners),[n("k-icon",{staticClass:"k-block-icon",attrs:{type:t.icon}}),n("span",{staticClass:"k-block-name"},[t._v(" "+t._s(t.name)+" ")]),t.label?n("span",{staticClass:"k-block-label"},[t._v(" "+t._s(t.label)+" ")]):t._e()],1)}),[],!1,Qg,null,null,null);function Qg(t){for(let e in Xg)this[e]=Xg[e]}var tk=function(){return Zg.exports}();const ek={};var nk=Rt({inheritAttrs:!1,props:{content:[Object,Array],fieldset:Object},methods:{field(t,e=null){let n=null;return Object.values(this.fieldset.tabs).forEach((e=>{e.fields[t]&&(n=e.fields[t])})),n||e},open(){this.$emit("open")},update(t){this.$emit("update",a(a({},this.content),t))}}},undefined,undefined,!1,sk,null,null,null);function sk(t){for(let e in ek)this[e]=ek[e]}var ik=function(){return nk.exports}();u.component("k-block",Eg),u.component("k-blocks",Bg),u.component("k-block-figure",Fg),u.component("k-block-options",Kg),u.component("k-block-selector",Wg),u.component("k-block-title",tk),u.component("k-block-type",ik);const ok={"./Types/Code.vue":_m,"./Types/Default.vue":Om,"./Types/Gallery.vue":Im,"./Types/Heading.vue":Pm,"./Types/Image.vue":zm,"./Types/Line.vue":Jm,"./Types/List.vue":Zm,"./Types/Markdown.vue":sg,"./Types/Quote.vue":lg,"./Types/Table.vue":hg,"./Types/Text.vue":vg,"./Types/Video.vue":wg};Object.keys(ok).map((t=>{const e=t.match(/\/([a-zA-Z]*)\.vue/)[1].toLowerCase();let n=ok[t].default;n.extends=ik,u.component("k-block-type-"+e,n)})),u.component("k-dialog",Ut),u.component("k-error-dialog",Wt),u.component("k-fiber-dialog",te),u.component("k-files-dialog",oe),u.component("k-form-dialog",ce),u.component("k-language-dialog",fe),u.component("k-pages-dialog",ve),u.component("k-remove-dialog",we),u.component("k-text-dialog",Oe),u.component("k-users-dialog",Me),u.component("k-drawer",De),u.component("k-form-drawer",Re),u.component("k-calendar",We),u.component("k-counter",en),u.component("k-autocomplete",Ue),u.component("k-form",an),u.component("k-form-buttons",pn),u.component("k-form-indicator",gn),u.component("k-field",Mn),u.component("k-fieldset",Bn),u.component("k-input",Fn),u.component("k-login",Un),u.component("k-login-code",Vn),u.component("k-times",Qn),u.component("k-upload",is),u.component("k-writer",Xs),u.component("k-login-alert",ei),u.component("k-checkbox-input",ri),u.component("k-checkboxes-input",di),u.component("k-date-input",gi),u.component("k-email-input",Ci),u.component("k-list-input",Ii),u.component("k-multiselect-input",Pi),u.component("k-number-input",zi),u.component("k-password-input",Ji),u.component("k-radio-input",Zi),u.component("k-range-input",so),u.component("k-select-input",lo),u.component("k-slug-input",fo),u.component("k-tags-input",bo),u.component("k-tel-input",xo),u.component("k-text-input",$i),u.component("k-textarea-input",To),u.component("k-time-input",jo),u.component("k-toggle-input",qo),u.component("k-url-input",Ho),u.component("k-blocks-field",Vo),u.component("k-checkboxes-field",tr),u.component("k-date-field",or),u.component("k-email-field",cr),u.component("k-files-field",gr),u.component("k-gap-field",yr),u.component("k-headline-field",Sr),u.component("k-info-field",Tr),u.component("k-layout-field",Kr),u.component("k-line-field",Wr),u.component("k-list-field",ea),u.component("k-multiselect-field",oa),u.component("k-number-field",ua),u.component("k-pages-field",ha),u.component("k-password-field",ka),u.component("k-radio-field",$a),u.component("k-range-field",Sa),u.component("k-select-field",Ta),u.component("k-slug-field",ja),u.component("k-structure-field",Ra),u.component("k-tags-field",Ha),u.component("k-text-field",Za),u.component("k-textarea-field",nl),u.component("k-tel-field",Ga),u.component("k-time-field",al),u.component("k-toggle-field",dl),u.component("k-url-field",gl),u.component("k-users-field",yl),u.component("k-writer-field",xl),u.component("k-toolbar",Al),u.component("k-toolbar-email-dialog",jl),u.component("k-toolbar-link-dialog",Nl),u.component("k-date-field-preview",zl),u.component("k-email-field-preview",Wl),u.component("k-files-field-preview",tu),u.component("k-list-field-preview",iu),u.component("k-pages-field-preview",lu),u.component("k-toggle-field-preview",ku),u.component("k-time-field-preview",pu),u.component("k-url-field-preview",Kl),u.component("k-users-field-preview",$u),u.component("k-writer-field-preview",Su),u.component("k-aspect-ratio",Au),u.component("k-bar",ju),u.component("k-box",qu),u.component("k-collection",Hu),u.component("k-column",Vu),u.component("k-dropzone",tc),u.component("k-empty",ic),u.component("k-file-preview",lc),u.component("k-grid",pc),u.component("k-header",kc),u.component("k-inside",$c),u.component("k-item",Oc),u.component("k-item-image",Ic),u.component("k-items",Pc),u.component("k-overlay",zc),u.component("k-panel",Kc),u.component("k-tabs",Wc),u.component("k-view",td),u.component("k-draggable",od),u.component("k-error-boundary",ud),u.component("k-fatal",hd),u.component("k-headline",kd),u.component("k-icon",$d),u.component("k-icons",Ed),u.component("k-image",Ld),u.component("k-loader",Pd),u.component("k-offline-warning",Fd),u.component("k-progress",Kd),u.component("k-registration",Wd),u.component("k-status-icon",op),u.component("k-sort-handle",tp),u.component("k-text",up),u.component("k-user-info",hp),u.component("k-breadcrumb",kp),u.component("k-button",_p),u.component("k-button-disabled",Op),u.component("k-button-group",Mp),u.component("k-button-link",Bp),u.component("k-button-native",zp),u.component("k-dropdown",Kp),u.component("k-dropdown-content",Xp),u.component("k-dropdown-item",nh),u.component("k-languages-dropdown",dh),u.component("k-link",ah),u.component("k-options-dropdown",gh),u.component("k-pagination",$h),u.component("k-prev-next",Ch),u.component("k-search",Ah),u.component("k-tag",Dh),u.component("k-topbar",Rh),u.component("k-sections",Uh),u.component("k-info-section",Vh),u.component("k-pages-section",tf),u.component("k-files-section",of),u.component("k-fields-section",uf),u.component("k-account-view",_f),u.component("k-error-view",Cf),u.component("k-file-view",Af),u.component("k-installation-view",Df),u.component("k-languages-view",qf),u.component("k-login-view",Kf),u.component("k-page-view",Wf),u.component("k-plugin-view",tm),u.component("k-reset-password-view",im),u.component("k-site-view",lm),u.component("k-system-view",hm),u.component("k-users-view",km),u.component("k-user-view",vf);u.config.productionTip=!1,u.config.devtools=!0,u.use(ct),u.use(Dt),u.use(Pt),u.use(qt),u.use(dt),u.use(Bt),u.use(vt),u.use(tt,ut),u.use(G),u.use(V),new u({store:ut,created(){window.panel.$vue=window.panel.app=this,window.panel.plugins.created.forEach((t=>t(this))),this.$store.dispatch("content/init")},render:t=>t(et)}).$mount("#app"); +import{V as t,a as e,m as s,d as n,c as i,b as o,I as r,P as l,S as a,F as u,N as c,s as d,l as p,w as h,e as m,f,t as g,g as k,h as b,i as y,j as v,k as $,n as _,D as x,o as w,E as S,p as C,q as O,r as A,T,u as I,v as M,x as E,y as L,z as j,A as D,B,C as P,G as N,H as q}from"./vendor.js";const F=t=>({changeName:async(e,s,n)=>t.patch(e+"/files/"+s+"/name",{name:n}),delete:async(e,s)=>t.delete(e+"/files/"+s),async get(e,s,n){let i=await t.get(e+"/files/"+s,n);return!0===Array.isArray(i.content)&&(i.content={}),i},link(t,e,s){return"/"+this.url(t,e,s)},update:async(e,s,n)=>t.patch(e+"/files/"+s,n),url(t,e,s){let n=t+"/files/"+e;return s&&(n+="/"+s),n}}),R=t=>({async blueprint(e){return t.get("pages/"+this.id(e)+"/blueprint")},async blueprints(e,s){return t.get("pages/"+this.id(e)+"/blueprints",{section:s})},async changeSlug(e,s){return t.patch("pages/"+this.id(e)+"/slug",{slug:s})},async changeStatus(e,s,n){return t.patch("pages/"+this.id(e)+"/status",{status:s,position:n})},async changeTemplate(e,s){return t.patch("pages/"+this.id(e)+"/template",{template:s})},async changeTitle(e,s){return t.patch("pages/"+this.id(e)+"/title",{title:s})},async children(e,s){return t.post("pages/"+this.id(e)+"/children/search",s)},async create(e,s){return null===e||"/"===e?t.post("site/children",s):t.post("pages/"+this.id(e)+"/children",s)},async delete(e,s){return t.delete("pages/"+this.id(e),s)},async duplicate(e,s,n){return t.post("pages/"+this.id(e)+"/duplicate",{slug:s,children:n.children||!1,files:n.files||!1})},async get(e,s){let n=await t.get("pages/"+this.id(e),s);return!0===Array.isArray(n.content)&&(n.content={}),n},id:t=>t.replace(/\//g,"+"),async files(e,s){return t.post("pages/"+this.id(e)+"/files/search",s)},link(t){return"/"+this.url(t)},async preview(t){return(await this.get(this.id(t),{select:"previewUrl"})).previewUrl},async search(e,s){return e?t.post("pages/"+this.id(e)+"/children/search?select=id,title,hasChildren",s):t.post("site/children/search?select=id,title,hasChildren",s)},async update(e,s){return t.patch("pages/"+this.id(e),s)},url(t,e){let s=null===t?"pages":"pages/"+String(t).replace(/\//g,"+");return e&&(s+="/"+e),s}});const z=t=>({running:0,async request(e,s,n=!1){s=Object.assign(s||{},{credentials:"same-origin",cache:"no-store",headers:{"x-requested-with":"xmlhttprequest","content-type":"application/json",...s.headers}}),t.methodOverwrite&&"GET"!==s.method&&"POST"!==s.method&&(s.headers["x-http-method-override"]=s.method,s.method="POST"),s=t.onPrepare(s);const i=e+"/"+JSON.stringify(s);t.onStart(i,n),this.running++;const o=await fetch([t.endpoint,e].join(t.endpoint.endsWith("/")||e.startsWith("/")?"":"/"),s);try{const e=await async function(t){const e=await t.text();let s;try{s=JSON.parse(e)}catch(n){return window.panel.$vue.$api.onParserError({html:e}),!1}return s}(o);if(o.status<200||o.status>299)throw e;if("error"===e.status)throw e;let s=e;return e.data&&"model"===e.type&&(s=e.data),this.running--,t.onComplete(i),t.onSuccess(e),s}catch(r){throw this.running--,t.onComplete(i),t.onError(r),r}},async get(t,e,s,n=!1){return e&&(t+="?"+Object.keys(e).filter((t=>void 0!==e[t]&&null!==e[t])).map((t=>t+"="+e[t])).join("&")),this.request(t,Object.assign(s||{},{method:"GET"}),n)},async post(t,e,s,n="POST",i=!1){return this.request(t,Object.assign(s||{},{method:n,body:JSON.stringify(e)}),i)},async patch(t,e,s,n=!1){return this.post(t,e,s,"PATCH",n)},async delete(t,e,s,n=!1){return this.post(t,e,s,"DELETE",n)}}),Y=t=>({blueprint:async e=>t.get("users/"+e+"/blueprint"),blueprints:async(e,s)=>t.get("users/"+e+"/blueprints",{section:s}),changeEmail:async(e,s)=>t.patch("users/"+e+"/email",{email:s}),changeLanguage:async(e,s)=>t.patch("users/"+e+"/language",{language:s}),changeName:async(e,s)=>t.patch("users/"+e+"/name",{name:s}),changePassword:async(e,s)=>t.patch("users/"+e+"/password",{password:s}),changeRole:async(e,s)=>t.patch("users/"+e+"/role",{role:s}),create:async e=>t.post("users",e),delete:async e=>t.delete("users/"+e),deleteAvatar:async e=>t.delete("users/"+e+"/avatar"),link(t,e){return"/"+this.url(t,e)},async list(e){return t.post(this.url(null,"search"),e)},get:async(e,s)=>t.get("users/"+e,s),async roles(e){return(await t.get(this.url(e,"roles"))).data.map((t=>({info:t.description||`(${window.panel.$t("role.description.placeholder")})`,text:t.title,value:t.name})))},search:async e=>t.post("users/search",e),update:async(e,s)=>t.patch("users/"+e,s),url(t,e){let s=t?"users/"+t:"users";return e&&(s+="/"+e),s}}),H={install(t,e){t.prototype.$api=t.$api=((t={})=>{const e={...{endpoint:"/api",methodOverwrite:!0,onPrepare:t=>t,onStart(){},onComplete(){},onSuccess(){},onParserError(){},onError(t){throw window.console.log(t.message),t}},...t.config||{}};let s={...e,...z(e),...t};return s.auth=(t=>({async login(e){const s={long:e.remember||!1,email:e.email,password:e.password};return t.post("auth/login",s)},logout:async()=>t.post("auth/logout"),user:async e=>t.get("auth",e),verifyCode:async e=>t.post("auth/code",{code:e})}))(s),s.files=F(s),s.languages=(t=>({create:async e=>t.post("languages",e),delete:async e=>t.delete("languages/"+e),get:async e=>t.get("languages/"+e),list:async()=>t.get("languages"),update:async(e,s)=>t.patch("languages/"+e,s)}))(s),s.pages=R(s),s.roles=(t=>({list:async e=>t.get("roles",e),get:async e=>t.get("roles/"+e)}))(s),s.system=(t=>({get:async(e={view:"panel"})=>t.get("system",e),install:async e=>(await t.post("system/install",e)).user,register:async e=>t.post("system/register",e)}))(s),s.site=(t=>({blueprint:async()=>t.get("site/blueprint"),blueprints:async()=>t.get("site/blueprints"),changeTitle:async e=>t.patch("site/title",{title:e}),children:async e=>t.post("site/children/search",e),get:async(e={view:"panel"})=>t.get("site",e),update:async e=>t.post("site",e)}))(s),s.translations=(t=>({list:async()=>t.get("translations"),get:async e=>t.get("translations/"+e)}))(s),s.users=Y(s),s})({config:{endpoint:window.panel.$urls.api,onComplete:s=>{t.$api.requests=t.$api.requests.filter((t=>t!==s)),0===t.$api.requests.length&&e.dispatch("isLoading",!1)},onError:e=>{window.panel.$config.debug&&window.console.error(e),403!==e.code||"Unauthenticated"!==e.message&&"access.panel"!==e.key||t.prototype.$go("/logout")},onParserError:({html:t,silent:s})=>{e.dispatch("fatal",{html:t,silent:s})},onPrepare:t=>(window.panel.$language&&(t.headers["x-language"]=window.panel.$language.code),t.headers["x-csrf"]=window.panel.$system.csrf,t),onStart:(s,n=!1)=>{!1===n&&e.dispatch("isLoading",!0),t.$api.requests.push(s)},onSuccess:()=>{clearInterval(t.$api.ping),t.$api.ping=setInterval(t.$api.auth.user,3e5)}},ping:null,requests:[]}),t.$api.ping=setInterval(t.$api.auth.user,3e5)}},U={name:"Fiber",data:()=>({component:null,state:window.fiber,key:null}),created(){this.$fiber.init(this.state,{base:document.querySelector("base").href,headers:()=>({"X-CSRF":this.state.$system.csrf}),onFatal({text:t,options:e}){this.$store.dispatch("fatal",{html:t,silent:e.silent})},onFinish:()=>{0===this.$api.requests.length&&this.$store.dispatch("isLoading",!1)},onPushState:t=>{window.history.pushState(t,"",t.$url)},onReplaceState:t=>{window.history.replaceState(t,"",t.$url)},onStart:({silent:t})=>{!0!==t&&this.$store.dispatch("isLoading",!0)},onSwap:async(t,e)=>{e={navigate:!0,replace:!1,...e},this.setGlobals(t),this.setTitle(t),this.setTranslation(t),this.component=t.$view.component,this.state=t,this.key=!0===e.replace?this.key:t.$view.timestamp,!0===e.navigate&&this.navigate()},query:()=>{var t;return{language:null==(t=this.state.$language)?void 0:t.code}}}),window.addEventListener("popstate",this.$reload)},methods:{navigate(){this.$store.dispatch("navigate")},setGlobals(e){["$config","$direction","$language","$languages","$license","$menu","$multilang","$permissions","$searches","$system","$translation","$urls","$user","$view"].forEach((s=>{void 0!==e[s]?t.prototype[s]=window.panel[s]=e[s]:t.prototype[s]=e[s]=window.panel[s]}))},setTitle(t){t.$view.title?document.title=t.$view.title+" | "+t.$system.title:document.title=t.$system.title},setTranslation(t){t.$translation&&(document.documentElement.lang=t.$translation.code)}},render(t){if(this.component)return t(this.component,{key:this.key,props:this.state.$view.props})}};function K(t){if(void 0!==t)return JSON.parse(JSON.stringify(t))}function G(t,e){for(const s of Object.keys(e))e[s]instanceof Object&&Object.assign(e[s],G(t[s]||{},e[s]));return Object.assign(t||{},e),t}const J={clone:K,isEmpty:function(t){return null==t||""===t||("object"==typeof t&&0===Object.keys(t).length&&t.constructor===Object||void 0!==t.length&&0===t.length)},merge:G},V=(t,e)=>{localStorage.setItem("kirby$content$"+t,JSON.stringify(e))},W={namespaced:!0,state:{current:null,models:{},status:{enabled:!0}},getters:{exists:t=>e=>Object.prototype.hasOwnProperty.call(t.models,e),hasChanges:(t,e)=>t=>{const s=e.model(t).changes;return Object.keys(s).length>0},isCurrent:t=>e=>t.current===e,id:t=>e=>(e=e||t.current,window.panel.$language?e+"?language="+window.panel.$language.code:e),model:(t,e)=>s=>(s=s||t.current,!0===e.exists(s)?t.models[s]:{api:null,originals:{},values:{},changes:{}}),originals:(t,e)=>t=>K(e.model(t).originals),values:(t,e)=>t=>({...e.originals(t),...e.changes(t)}),changes:(t,e)=>t=>K(e.model(t).changes)},mutations:{CLEAR(t){Object.keys(t.models).forEach((e=>{t.models[e].changes={}})),Object.keys(localStorage).forEach((t=>{t.startsWith("kirby$content$")&&localStorage.removeItem(t)}))},CREATE(e,[s,n]){if(!n)return!1;let i=e.models[s]?e.models[s].changes:n.changes;t.set(e.models,s,{api:n.api,originals:n.originals,changes:i||{}})},CURRENT(t,e){t.current=e},MOVE(e,[s,n]){const i=K(e.models[s]);t.delete(e.models,s),t.set(e.models,n,i);const o=localStorage.getItem("kirby$content$"+s);localStorage.removeItem("kirby$content$"+s),localStorage.setItem("kirby$content$"+n,o)},REMOVE(e,s){t.delete(e.models,s),localStorage.removeItem("kirby$content$"+s)},REVERT(e,s){e.models[s]&&(t.set(e.models[s],"changes",{}),localStorage.removeItem("kirby$content$"+s))},STATUS(e,s){t.set(e.status,"enabled",s)},UPDATE(e,[s,n,i]){if(!e.models[s])return!1;void 0===i&&(i=null),i=K(i);const o=JSON.stringify(i);JSON.stringify(e.models[s].originals[n])==o?t.delete(e.models[s].changes,n):t.set(e.models[s].changes,n,i),V(s,{api:e.models[s].api,originals:e.models[s].originals,changes:e.models[s].changes})}},actions:{init(t){Object.keys(localStorage).filter((t=>t.startsWith("kirby$content$"))).map((t=>t.split("kirby$content$")[1])).forEach((e=>{const s=localStorage.getItem("kirby$content$"+e);t.commit("CREATE",[e,JSON.parse(s)])})),Object.keys(localStorage).filter((t=>t.startsWith("kirby$form$"))).map((t=>t.split("kirby$form$")[1])).forEach((e=>{const s=localStorage.getItem("kirby$form$"+e);let n=null;try{n=JSON.parse(s)}catch(o){}if(!n||!n.api)return localStorage.removeItem("kirby$form$"+e),!1;const i={api:n.api,originals:n.originals,changes:n.values};t.commit("CREATE",[e,i]),V(e,i),localStorage.removeItem("kirby$form$"+e)}))},clear(t){t.commit("CLEAR")},create(t,e){const s=K(e.content);Array.isArray(e.ignore)&&e.ignore.forEach((t=>delete s[t])),e.id=t.getters.id(e.id);const n={api:e.api,originals:s,changes:{}};t.commit("CREATE",[e.id,n]),t.dispatch("current",e.id)},current(t,e){t.commit("CURRENT",e)},disable(t){t.commit("STATUS",!1)},enable(t){t.commit("STATUS",!0)},move(t,[e,s]){e=t.getters.id(e),s=t.getters.id(s),t.commit("MOVE",[e,s])},remove(t,e){t.commit("REMOVE",e),t.getters.isCurrent(e)&&t.commit("CURRENT",null)},revert(t,e){e=e||t.state.current,t.commit("REVERT",e)},async save(e,s){if(s=s||e.state.current,e.getters.isCurrent(s)&&!1===e.state.status.enabled)return!1;e.dispatch("disable");const n=e.getters.model(s),i={...n.originals,...n.changes};try{await t.$api.patch(n.api,i),e.commit("CREATE",[s,{...n,originals:i}]),e.dispatch("revert",s)}finally{e.dispatch("enable")}},update(t,[e,s,n]){n=n||t.state.current,t.commit("UPDATE",[n,e,s])}}},X={namespaced:!0,state:{open:[]},mutations:{CLOSE(t,e){t.open=e?t.open.filter((t=>t.id!==e)):[]},GOTO(t,e){t.open=t.open.slice(0,t.open.findIndex((t=>t.id===e))+1)},OPEN(t,e){t.open.push(e)}},actions:{close(t,e){t.commit("CLOSE",e)},goto(t,e){t.commit("GOTO",e)},open(t,e){t.commit("OPEN",e)}}},Z={timer:null,namespaced:!0,state:{type:null,message:null,details:null,timeout:null},mutations:{SET(t,e){t.type=e.type,t.message=e.message,t.details=e.details,t.timeout=e.timeout},UNSET(t){t.type=null,t.message=null,t.details=null,t.timeout=null}},actions:{close(t){clearTimeout(this.timer),t.commit("UNSET")},deprecated(t,e){console.warn("Deprecated: "+e)},error(t,e){let s=e;"string"==typeof e&&(s={message:e}),e instanceof Error&&(s={message:e.message},window.panel.$config.debug&&window.console.error(e)),t.dispatch("dialog",{component:"k-error-dialog",props:s},{root:!0}),t.dispatch("close")},open(t,e){t.dispatch("close"),t.commit("SET",e),e.timeout&&(this.timer=setTimeout((()=>{t.dispatch("close")}),e.timeout))},success(t,e){"string"==typeof e&&(e={message:e}),t.dispatch("open",{type:"success",timeout:4e3,...e})}}};t.use(e);const Q=new e.Store({strict:!1,state:{dialog:null,drag:null,fatal:!1,isLoading:!1},mutations:{SET_DIALOG(t,e){t.dialog=e},SET_DRAG(t,e){t.drag=e},SET_FATAL(t,e){t.fatal=e},SET_LOADING(t,e){t.isLoading=e}},actions:{dialog(t,e){t.commit("SET_DIALOG",e)},drag(t,e){t.commit("SET_DRAG",e)},fatal(t,e){!1!==e?(console.error("The JSON response could not be parsed"),window.panel.$config.debug&&console.info(e.html),e.silent||t.commit("SET_FATAL",e.html)):t.commit("SET_FATAL",!1)},isLoading(t,e){t.commit("SET_LOADING",!0===e)},navigate(t){t.dispatch("dialog",null),t.dispatch("drawers/close")}},modules:{content:W,drawers:X,notification:Z}}),tt={install(t){window.panel=window.panel||{},window.onunhandledrejection=t=>{t.preventDefault(),Q.dispatch("notification/error",t.reason)},window.panel.deprecated=t=>{Q.dispatch("notification/deprecated",t)},window.panel.error=t.config.errorHandler=t=>{Q.dispatch("notification/error",t)}}},et={install(t){const e=s(),n={$on:e.on,$off:e.off,$emit:e.emit,blur(t){n.$emit("blur",t)},click(t){n.$emit("click",t)},copy(t){n.$emit("copy",t)},dragenter(t){n.entered=t.target,n.prevent(t),n.$emit("dragenter",t)},dragleave(t){n.prevent(t),n.entered===t.target&&n.$emit("dragleave",t)},drop(t){n.prevent(t),n.$emit("drop",t)},entered:null,focus(t){n.$emit("focus",t)},keydown(e){let s=["keydown"];(e.metaKey||e.ctrlKey)&&s.push("cmd"),!0===e.altKey&&s.push("alt"),!0===e.shiftKey&&s.push("shift");let i=t.prototype.$helper.string.lcfirst(e.key);const o={escape:"esc",arrowUp:"up",arrowDown:"down",arrowLeft:"left",arrowRight:"right"};o[i]&&(i=o[i]),!1===["alt","control","shift","meta"].includes(i)&&s.push(i),n.$emit(s.join("."),e),n.$emit("keydown",e)},keyup(t){n.$emit("keyup",t)},online(t){n.$emit("online",t)},offline(t){n.$emit("offline",t)},paste(t){n.$emit("paste",t)},prevent(t){t.stopPropagation(),t.preventDefault()}};document.addEventListener("click",n.click,!1),document.addEventListener("copy",n.copy,!0),document.addEventListener("focus",n.focus,!0),document.addEventListener("paste",n.paste,!0),window.addEventListener("blur",n.blur,!1),window.addEventListener("dragenter",n.dragenter,!1),window.addEventListener("dragexit",n.prevent,!1),window.addEventListener("dragleave",n.dragleave,!1),window.addEventListener("drop",n.drop,!1),window.addEventListener("dragover",n.prevent,!1),window.addEventListener("keydown",n.keydown,!1),window.addEventListener("keyup",n.keyup,!1),window.addEventListener("offline",n.offline),window.addEventListener("online",n.online),t.prototype.$events=n}};class st{constructor(t={}){this.options={base:"/",headers:()=>({}),onFatal:()=>{},onFinish:()=>{},onPushState:()=>{},onReplaceState:()=>{},onStart:()=>{},onSwap:()=>{},query:()=>({}),...t},this.state={}}init(t={},e={}){this.options={...this.options,...e},this.setState(t)}arrayToString(t){return!1===Array.isArray(t)?String(t):t.join(",")}body(t){return"object"==typeof t?JSON.stringify(t):t}async fetch(t,e){return fetch(t,e)}async go(t,e){try{const s=await this.request(t,e);return!1!==s&&this.setState(s,e)}catch(s){if(!0!==(null==e?void 0:e.silent))throw s}}query(t={},e={}){let s=new URLSearchParams(e);return"object"!=typeof t&&(t={}),Object.entries(t).forEach((([t,e])=>{null!==e&&s.set(t,e)})),Object.entries(this.options.query()).forEach((([t,e])=>{var n,i;null!==(e=null!=(i=null!=(n=s.get(t))?n:e)?i:null)&&s.set(t,e)})),s}redirect(t){window.location.href=t}reload(t={}){return this.go(window.location.href,{...t,replace:!0})}async request(t="",e={}){var s;const n=!!(e={globals:!1,method:"GET",only:[],query:{},silent:!1,type:"$view",...e}).globals&&this.arrayToString(e.globals),i=this.arrayToString(e.only);this.options.onStart(e);try{const r=this.url(t,e.query);if(new URL(r).origin!==location.origin)return this.redirect(r),!1;const l=await this.fetch(r,{method:e.method,body:this.body(e.body),credentials:"same-origin",cache:"no-store",headers:{...this.options.headers(),"X-Fiber":!0,"X-Fiber-Globals":n,"X-Fiber-Only":i,"X-Fiber-Referrer":(null==(s=this.state.$view)?void 0:s.path)||null,...e.headers}});if(!1===l.headers.has("X-Fiber"))return this.redirect(l.url),!1;const a=await l.text();let u;try{u=JSON.parse(a)}catch(o){return this.options.onFatal({url:r,path:t,options:e,response:l,text:a}),!1}if(!u[e.type])throw Error(`The ${e.type} could not be loaded`);const c=u[e.type];if(c.error)throw Error(c.error);return"$view"===e.type?i.length?G(this.state,u):u:c}finally{this.options.onFinish(e)}}async setState(t,e={}){return"object"==typeof t&&(this.state=K(t),!0===e.replace||this.url(this.state.$url).href===window.location.href?this.options.onReplaceState(this.state,e):this.options.onPushState(this.state,e),this.options.onSwap(this.state,e),this.state)}url(t="",e={}){return(t="string"==typeof t&&null===t.match(/^https?:\/\//)?new URL(this.options.base+t.replace(/^\//,"")):new URL(t)).search=this.query(e,t.search),t}}const nt=async function(t){return{cancel:null,submit:null,props:{},...t}},it=async function(t,e={}){let s=null,n=null;"function"==typeof e?(s=e,e={}):(s=e.submit,n=e.cancel);let i=await this.$fiber.request("dialogs/"+t,{...e,type:"$dialog"});return"object"==typeof i&&(i.submit=s||null,i.cancel=n||null,i)};async function ot(t,e={}){let s=null;if(s="object"==typeof t?await nt.call(this,t):await it.call(this,t,e),!s)return!1;if(!s.component||!1===this.$helper.isComponent(s.component))throw Error("The dialog component does not exist");return s.props=s.props||{},this.$store.dispatch("dialog",s),s}function rt(t,e={}){return async s=>{const n=await this.$fiber.request("dropdowns/"+t,{...e,type:"$dropdown"});if(!n)return!1;if(!1===Array.isArray(n.options)||0===n.options.length)throw Error("The dropdown is empty");n.options.map((t=>(t.dialog&&(t.click=()=>{const e="string"==typeof t.dialog?t.dialog:t.dialog.url,s="object"==typeof t.dialog?t.dialog:{};return this.$dialog(e,s)}),t))),s(n.options)}}async function lt(t,e,s={}){return await this.$fiber.request("search/"+t,{query:{query:e},type:"$search",...s})}const at={install(t){const e=new st;t.prototype.$fiber=window.panel.$fiber=e,t.prototype.$dialog=window.panel.$dialog=ot,t.prototype.$dropdown=window.panel.$dropdown=rt,t.prototype.$go=window.panel.$go=e.go.bind(e),t.prototype.$reload=window.panel.$reload=e.reload.bind(e),t.prototype.$request=window.panel.$request=e.request.bind(e),t.prototype.$search=window.panel.$search=lt,t.prototype.$url=window.panel.$url=e.url.bind(e)}};const ut={read:function(t){if(!t)return null;if("string"==typeof t)return t;if(t instanceof ClipboardEvent){t.preventDefault();const e=t.clipboardData.getData("text/html")||t.clipboardData.getData("text/plain")||null;if(e)return e.replace(/\u00a0/g," ")}return null},write:function(t,e){if("string"!=typeof t&&(t=JSON.stringify(t,null,2)),e&&e instanceof ClipboardEvent)return e.preventDefault(),e.clipboardData.setData("text/plain",t),!0;const s=document.createElement("textarea");if(s.value=t,document.body.append(s),navigator.userAgent.match(/ipad|ipod|iphone/i)){s.contentEditable=!0,s.readOnly=!0;const t=document.createRange();t.selectNodeContents(s);const e=window.getSelection();e.removeAllRanges(),e.addRange(t),s.setSelectionRange(0,999999)}else s.select();return document.execCommand("copy"),s.remove(),!0}};function ct(t){if("string"==typeof t){if("pattern"===(t=t.toLowerCase()))return"var(--color-gray-800) var(--bg-pattern)";if(!1===t.startsWith("#")&&!1===t.startsWith("var(")){const e="--color-"+t;if(window.getComputedStyle(document.documentElement).getPropertyValue(e))return`var(${e})`}return t}}const dt=(t,e)=>{let s=null;return function(){clearTimeout(s),s=setTimeout((()=>t.apply(this,arguments)),e)}};function pt(t,e=!1){if(!t.match("youtu"))return!1;let s=null;try{s=new URL(t)}catch(d){return!1}const n=s.pathname.split("/").filter((t=>""!==t)),i=n[0],o=n[1],r="https://"+(!0===e?"www.youtube-nocookie.com":s.host)+"/embed",l=t=>!!t&&null!==t.match(/^[a-zA-Z0-9_-]+$/);let a=s.searchParams,u=null;switch(n.join("/")){case"embed/videoseries":case"playlist":l(a.get("list"))&&(u=r+"/videoseries");break;case"watch":l(a.get("v"))&&(u=r+"/"+a.get("v"),a.has("t")&&a.set("start",a.get("t")),a.delete("v"),a.delete("t"));break;default:s.host.includes("youtu.be")&&l(i)?(u="https://www.youtube.com/embed/"+i,a.has("t")&&a.set("start",a.get("t")),a.delete("t")):"embed"===i&&l(o)&&(u=r+"/"+o)}if(!u)return!1;const c=a.toString();return c.length&&(u+="?"+c),u}function ht(t,e=!1){let s=null;try{s=new URL(t)}catch(a){return!1}const n=s.pathname.split("/").filter((t=>""!==t));let i=s.searchParams,o=null;switch(!0===e&&i.append("dnt",1),s.host){case"vimeo.com":case"www.vimeo.com":o=n[0];break;case"player.vimeo.com":o=n[1]}if(!o||!o.match(/^[0-9]*$/))return!1;let r="https://player.vimeo.com/video/"+o;const l=i.toString();return l.length&&(r+="?"+l),r}const mt={youtube:pt,vimeo:ht,video:function(t,e=!1){return t.includes("youtu")?pt(t,e):!!t.includes("vimeo")&&ht(t,e)}},ft=e=>void 0!==t.options.components[e],gt=t=>!!t.dataTransfer&&(!!t.dataTransfer.types&&(!0===t.dataTransfer.types.includes("Files")&&!1===t.dataTransfer.types.includes("text/plain")));const kt={metaKey:function(){return window.navigator.userAgent.indexOf("Mac")>-1?"cmd":"ctrl"}},bt=(t="3/2",e="100%",s=!0)=>{const n=String(t).split("/");if(2!==n.length)return e;const i=Number(n[0]),o=Number(n[1]);let r=100;return 0!==i&&0!==o&&(r=s?r/i*o:r/o*i,r=parseFloat(String(r)).toFixed(2)),r+"%"},yt=t=>{var e=(t=t||{}).desc?-1:1,s=-e,n=/^0/,i=/\s+/g,o=/^\s+|\s+$/g,r=/[^\x00-\x80]/,l=/^0x[0-9a-f]+$/i,a=/(0x[\da-fA-F]+|(^[\+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|\d+)/g,u=/(^([\w ]+,?[\w ]+)?[\w ]+,?[\w ]+\d+:\d+(:\d+)?[\w ]?|^\d{1,4}[\/\-]\d{1,4}[\/\-]\d{1,4}|^\w+, \w+ \d+, \d{4})/,c=t.insensitive?function(t){return function(t){if(t.toLocaleLowerCase)return t.toLocaleLowerCase();return t.toLowerCase()}(""+t).replace(o,"")}:function(t){return(""+t).replace(o,"")};function d(t){return t.replace(a,"\0$1\0").replace(/\0$/,"").replace(/^\0/,"").split("\0")}function p(t,e){return(!t.match(n)||1===e)&&parseFloat(t)||t.replace(i," ").replace(o,"")||0}return function(t,n){var i=c(t),o=c(n);if(!i&&!o)return 0;if(!i&&o)return s;if(i&&!o)return e;var a=d(i),h=d(o),m=parseInt(i.match(l),16)||1!==a.length&&Date.parse(i),f=parseInt(o.match(l),16)||m&&o.match(u)&&Date.parse(o)||null;if(f){if(mf)return e}for(var g=a.length,k=h.length,b=0,y=Math.max(g,k);b0)return e;if(_<0)return s;if(b===y-1)return 0}else{if(v<$)return s;if(v>$)return e}}return 0}};function vt(t){const e={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};return String(t).replace(/[&<>"'`=/]/g,(t=>e[t]))}function $t(t,e={}){const s=(t,e={})=>{var n;const i=vt(t.shift()),o=null!=(n=e[i])?n:null;return null===o?Object.prototype.hasOwnProperty.call(e,i)||"…":0===t.length?o:s(t,o)},n="[{]{1,2}[\\s]?",i="[\\s]?[}]{1,2}";return(t=t.replace(new RegExp(`${n}(.*?)${i}`,"gi"),((t,n)=>s(n.split("."),e)))).replace(new RegExp(`${n}.*${i}`,"gi"),"…")}function _t(t){const e=String(t);return e.charAt(0).toUpperCase()+e.slice(1)}RegExp.escape=function(t){return t.replace(new RegExp("[-/\\\\^$*+?.()[\\]{}]","gu"),"\\$&")};const xt={camelToKebab:function(t){return t.replace(/([a-z0-9])([A-Z])/g,"$1-$2").toLowerCase()},escapeHTML:vt,hasEmoji:function(t){if("string"!=typeof t)return!1;const e=t.match(/(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|[\ud83c\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|[\ud83c\ude32-\ude3a]|[\ud83c\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])/i);return null!==e&&null!==e.length},lcfirst:function(t){const e=String(t);return e.charAt(0).toLowerCase()+e.slice(1)},pad:function(t,e=2){t=String(t);let s="";for(;s.length]+)>)/gi,"")},template:$t,ucfirst:_t,ucwords:function(t){return String(t).split(/ /g).map((t=>_t(t))).join(" ")},uuid:function(){let t,e,s="";for(t=0;t<32;t++)e=16*Math.random()|0,8!=t&&12!=t&&16!=t&&20!=t||(s+="-"),s+=(12==t?4:16==t?3&e|8:e).toString(16);return s}},wt=(t,e)=>{const s=Object.assign({url:"/",field:"file",method:"POST",attributes:{},complete:function(){},error:function(){},success:function(){},progress:function(){}},e),n=new FormData;n.append(s.field,t,t.name),s.attributes&&Object.keys(s.attributes).forEach((t=>{n.append(t,s.attributes[t])}));const i=new XMLHttpRequest,o=e=>{if(!e.lengthComputable||!s.progress)return;let n=Math.max(0,Math.min(100,e.loaded/e.total*100));s.progress(i,t,Math.ceil(n))};i.upload.addEventListener("loadstart",o),i.upload.addEventListener("progress",o),i.addEventListener("load",(e=>{let n=null;try{n=JSON.parse(e.target.response)}catch(o){n={status:"error",message:"The file could not be uploaded"}}"error"===n.status?s.error(i,t,n):(s.success(i,t,n),s.progress(i,t,100))})),i.addEventListener("error",(e=>{const n=JSON.parse(e.target.response);s.error(i,t,n),s.progress(i,t,100)})),i.open(s.method,s.url,!0),s.headers&&Object.keys(s.headers).forEach((t=>{const e=s.headers[t];i.setRequestHeader(t,e)})),i.send(n)},St={install(t){Array.prototype.sortBy=function(e){const s=t.prototype.$helper.sort(),n=e.split(" "),i=n[0],o=n[1]||"asc";return this.sort(((t,e)=>{const n=String(t[i]).toLowerCase(),r=String(e[i]).toLowerCase();return"desc"===o?s(r,n):s(n,r)}))},t.prototype.$helper={clipboard:ut,clone:J.clone,color:ct,embed:mt,isComponent:ft,isUploadEvent:gt,debounce:dt,keyboard:kt,object:J,pad:xt.pad,ratio:bt,slug:xt.slug,sort:yt,string:xt,upload:wt,uuid:xt.uuid},t.prototype.$esc=xt.escapeHTML}},Ct={install(t){t.$t=t.prototype.$t=window.panel.$t=(t,e,s=null)=>{if("string"!=typeof t)return;const n=window.panel.$translation.data[t]||s;return"string"!=typeof n?n:$t(n,e)},t.directive("direction",{inserted(t,e,s){!0!==s.context.disabled?t.dir=s.context.$direction:t.dir=null}})}};n.extend(i),n.extend(((t,e,s)=>{s.interpret=(t,e="date")=>{const n={date:{"YYYY-MM-DD":!0,"YYYY-MM-D":!0,"YYYY-MM-":!0,"YYYY-MM":!0,"YYYY-M-DD":!0,"YYYY-M-D":!0,"YYYY-M-":!0,"YYYY-M":!0,"YYYY-":!0,YYYYMMDD:!0,"MMM DD YYYY":!1,"MMM D YYYY":!1,"MMM DD YY":!1,"MMM D YY":!1,"MMM DD":!1,"MMM D":!1,"DD MMMM YYYY":!1,"DD MMMM YY":!1,"DD MMMM":!1,"D MMMM YYYY":!1,"D MMMM YY":!1,"D MMMM":!1,"DD MMM YYYY":!1,"D MMM YYYY":!1,"DD MMM YY":!1,"D MMM YY":!1,"DD MMM":!1,"D MMM":!1,"DD MM YYYY":!1,"DD M YYYY":!1,"D MM YYYY":!1,"D M YYYY":!1,"DD MM YY":!1,"D MM YY":!1,"DD M YY":!1,"D M YY":!1,YYYY:!0,MMMM:!0,MMM:!0,"DD MM":!1,"DD M":!1,"D MM":!1,"D M":!1,DD:!1,D:!1},time:{"HH:mm:ss a":!1,"HH:mm:ss":!1,"HH:mm a":!1,"HH:mm":!1,"HH a":!1,HH:!1}};if("string"==typeof t&&""!==t)for(const i in n[e]){const o=s(t,i,n[e][i]);if(!0===o.isValid())return o}return null}})),n.extend(((t,e,s)=>{const n=t=>"date"===t?"YYYY-MM-DD":"time"===t?"HH:mm:ss":"YYYY-MM-DD HH:mm:ss";e.prototype.toISO=function(t="datetime"){return this.format(n(t))},s.iso=function(t,e="datetime"){const i=s(t,n(e));return i&&i.isValid()?i:null}})),n.extend(((t,e)=>{e.prototype.merge=function(t,e="date"){let s=this.clone();if(!t||!t.isValid())return this;if("string"==typeof e){const t={date:["year","month","date"],time:["hour","minute","second"]};if(!1===Object.prototype.hasOwnProperty.call(t,e))throw new Error("Invalid merge unit alias");e=t[e]}for(const n of e)s=s.set(n,t.get(n));return s}})),n.extend(((t,e,s)=>{s.pattern=t=>new class{constructor(t,e){this.dayjs=t,this.pattern=e;const s={year:["YY","YYYY"],month:["M","MM","MMM","MMMM"],day:["D","DD"],hour:["h","hh","H","HH"],minute:["m","mm"],second:["s","ss"],meridiem:["a"]};this.parts=this.pattern.split(/\W/).map(((t,e)=>{const n=this.pattern.indexOf(t);return{index:e,unit:Object.keys(s)[Object.values(s).findIndex((e=>e.includes(t)))],start:n,end:n+(t.length-1)}}))}at(t,e=t){const s=this.parts.filter((s=>s.start<=t&&s.end>=e-1));return s[0]?s[0]:this.parts.filter((e=>e.start<=t)).pop()}format(t){return t&&t.isValid()?t.format(this.pattern):null}}(s,t)})),n.extend(((t,e)=>{e.prototype.round=function(t="date",e=1){const s=["second","minute","hour","date","month","year"];if("day"===t&&(t="date"),!1===s.includes(t))throw new Error("Invalid rounding unit");if(["date","month","year"].includes(t)&&1!==e||"hour"===t&&24%e!=0||["second","minute"].includes(t)&&60%e!=0)throw"Invalid rounding size for "+t;let n=this.clone();const i=s.indexOf(t),o=s.slice(0,i),r=o.pop();if(o.forEach((t=>n=n.startOf(t))),r){const e={month:12,date:n.daysInMonth(),hour:24,minute:60,second:60}[r];Math.round(n.get(r)/e)*e===e&&(n=n.add(1,"date"===t?"day":t)),n=n.startOf(t)}return n=n.set(t,Math.round(n.get(t)/e)*e),n}})),n.extend(((t,e,s)=>{e.prototype.validate=function(t,e,n="day"){if(!this.isValid())return!1;if(!t)return!0;t=s.iso(t);const i={min:"isAfter",max:"isBefore"}[e];return this.isSame(t,n)||this[i](t,n)}}));const Ot={install(t){t.prototype.$library={autosize:o,dayjs:n}}},At={props:{blueprint:String,lock:[Boolean,Object],help:String,name:String,parent:String,timestamp:Number},methods:{load(){return this.$api.get(this.parent+"/sections/"+this.name)}}},Tt={install(t){const e={...t.options.components},s={section:At};for(const[n,i]of Object.entries(window.panel.plugins.components))i.template||i.render||i.extends?("string"==typeof(null==i?void 0:i.extends)&&(e[i.extends]?i.extends=e[i.extends].extend({options:i,components:{...e,...i.components||{}}}):i.extends=null),i.template&&(i.render=null),i.mixins&&(i.mixins=i.mixins.map((t=>"string"==typeof t?s[t]:t))),e[n]&&window.console.warn(`Plugin is replacing "${n}"`),t.component(n,i),e[n]=t.options.components[n]):Q.dispatch("notification/error",`Neither template or render method provided nor extending a component when loading plugin component "${n}". The component has not been registered.`);for(const n of window.panel.plugins.use)t.use(n)}};function It(t,e,s,n,i,o,r,l){var a,u="function"==typeof t?t.options:t;if(e&&(u.render=e,u.staticRenderFns=s,u._compiled=!0),n&&(u.functional=!0),o&&(u._scopeId="data-v-"+o),r?(a=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),i&&i.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(r)},u._ssrRegister=a):i&&(a=l?function(){i.call(this,(u.functional?this.parent:this).$root.$options.shadowRoot)}:i),a)if(u.functional){u._injectStyles=a;var c=u.render;u.render=function(t,e){return a.call(e),c(t,e)}}else{var d=u.beforeCreate;u.beforeCreate=d?[].concat(d,a):[a]}return{exports:t,options:u}}const Mt=It({props:{autofocus:{type:Boolean,default:!0},cancelButton:{type:[String,Boolean],default:!0},icon:{type:String,default:"check"},size:{type:String,default:"default"},submitButton:{type:[String,Boolean],default:!0},theme:String,visible:Boolean},data:()=>({notification:null}),computed:{buttons(){let t=[];return this.cancelButton&&t.push({icon:"cancel",text:this.cancelButtonLabel,class:"k-dialog-button-cancel",click:this.cancel}),this.submitButtonConfig&&t.push({icon:this.icon,text:this.submitButtonLabel,theme:this.theme,class:"k-dialog-button-submit",click:this.submit}),t},cancelButtonLabel(){return!1!==this.cancelButton&&(!0===this.cancelButton||0===this.cancelButton.length?this.$t("cancel"):this.cancelButton)},submitButtonConfig(){return void 0!==this.$attrs.button?this.$attrs.button:void 0===this.submitButton||this.submitButton},submitButtonLabel(){return!0===this.submitButton||0===this.submitButton.length?this.$t("confirm"):this.submitButton}},created(){this.$events.$on("keydown.esc",this.close,!1)},destroyed(){this.$events.$off("keydown.esc",this.close,!1)},mounted(){this.visible&&this.$nextTick(this.open)},methods:{onOverlayClose(){this.notification=null,this.$emit("close"),this.$events.$off("keydown.esc",this.close),this.$store.dispatch("dialog",!1)},open(){this.$store.state.dialog||this.$store.dispatch("dialog",!0),this.notification=null,this.$refs.overlay.open(),this.$emit("open"),this.$events.$on("keydown.esc",this.close)},close(){this.$refs.overlay&&this.$refs.overlay.close()},cancel(){this.$emit("cancel"),this.close()},focus(){var t;if(null==(t=this.$refs.dialog)?void 0:t.querySelector){const t=this.$refs.dialog.querySelector(".k-dialog-button-cancel");"function"==typeof(null==t?void 0:t.focus)&&t.focus()}},error(t){this.notification={message:t,type:"error"}},submit(){this.$emit("submit")},success(t){this.notification={message:t,type:"success"}}}},(function(){var t=this,e=t._self._c;return e("k-overlay",{ref:"overlay",attrs:{autofocus:t.autofocus,centered:!0},on:{close:t.onOverlayClose,ready:function(e){return t.$emit("ready")}}},[e("div",{ref:"dialog",staticClass:"k-dialog",class:t.$vnode.data.staticClass,attrs:{"data-size":t.size},on:{mousedown:function(t){t.stopPropagation()}}},[t.notification?e("div",{staticClass:"k-dialog-notification",attrs:{"data-theme":t.notification.type}},[e("p",[t._v(t._s(t.notification.message))]),e("k-button",{attrs:{icon:"cancel"},on:{click:function(e){t.notification=null}}})],1):t._e(),e("div",{staticClass:"k-dialog-body scroll-y-auto"},[t._t("default")],2),t.$slots.footer||t.buttons.length?e("footer",{staticClass:"k-dialog-footer"},[t._t("footer",(function(){return[e("k-button-group",{attrs:{buttons:t.buttons}})]}))],2):t._e()])])}),[],!1,null,null,null,null).exports,Et={props:{autofocus:{type:Boolean,default:!0},cancelButton:{type:[String,Boolean],default:!0},icon:String,submitButton:{type:[String,Boolean],default:!0},size:String,theme:String,visible:Boolean},methods:{close(){this.$refs.dialog.close(),this.$emit("close")},error(t){this.$refs.dialog.error(t)},open(){this.$refs.dialog.open(),this.$emit("open")},success(t){this.$refs.dialog.close(),t.route&&this.$go(t.route),t.message&&this.$store.dispatch("notification/success",t.message),t.event&&("string"==typeof t.event&&(t.event=[t.event]),t.event.forEach((e=>{this.$events.$emit(e,t)}))),!1!==Object.prototype.hasOwnProperty.call(t,"emit")&&!1===t.emit||this.$emit("success")}}};const Lt=It({mixins:[Et],props:{details:[Object,Array],message:String,size:{type:String,default:"medium"}},computed:{detailsList(){return Array.isArray(this.details)?this.details:Object.values(this.details||{})}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",staticClass:"k-error-dialog",attrs:{"cancel-button":!1,size:t.size,visible:!0},on:{cancel:function(e){return t.$emit("cancel")},close:function(e){return t.$emit("close")},submit:function(e){return t.$refs.dialog.close()}}},[e("k-text",[t._v(t._s(t.message))]),t.detailsList.length?e("dl",{staticClass:"k-error-details"},[t._l(t.detailsList,(function(s,n){return[e("dt",{key:"detail-label-"+n},[t._v(" "+t._s(s.label)+" ")]),e("dd",{key:"detail-message-"+n},["object"==typeof s.message?[e("ul",t._l(s.message,(function(s,n){return e("li",{key:n},[t._v(" "+t._s(s)+" ")])})),0)]:[t._v(" "+t._s(s.message)+" ")]],2)]}))],2):t._e()],1)}),[],!1,null,null,null,null).exports;const jt=It({props:{code:Number,component:String,path:String,props:Object,referrer:String},methods:{close(){this.$refs.dialog.close()},onCancel(){"function"==typeof this.$store.state.dialog.cancel&&this.$store.state.dialog.cancel({dialog:this})},async onSubmit(t){let e=null;try{if("function"==typeof this.$store.state.dialog.submit)e=await this.$store.state.dialog.submit({dialog:this,value:t});else{if(!this.path)throw"The dialog needs a submit action or a dialog route path to be submitted";e=await this.$request(this.path,{body:t,method:"POST",type:"$dialog",headers:{"X-Fiber-Referrer":this.referrer}})}if(!1===e)return!1;this.close(),this.$store.dispatch("notification/success",":)"),e.event&&("string"==typeof e.event&&(e.event=[e.event]),e.event.forEach((t=>{this.$events.$emit(t,e)}))),e.dispatch&&Object.keys(e.dispatch).forEach((t=>{const s=e.dispatch[t];this.$store.dispatch(t,!0===Array.isArray(s)?[...s]:s)})),e.redirect?this.$go(e.redirect):this.$reload(e.reload||{})}catch(s){this.$refs.dialog.error(s)}}}},(function(){var t=this;return(0,t._self._c)(t.component,t._b({ref:"dialog",tag:"component",attrs:{visible:!0},on:{cancel:t.onCancel,submit:t.onSubmit}},"component",t.props,!1))}),[],!1,null,null,null,null).exports,Dt={data:()=>({models:[],issue:null,selected:{},options:{endpoint:null,max:null,multiple:!0,parent:null,selected:[],search:!0},search:null,pagination:{limit:20,page:1,total:0}}),computed:{checkedIcon(){return!0===this.multiple?"check":"circle-filled"},collection(){return{empty:this.emptyProps,items:this.items,link:!1,layout:"list",pagination:{details:!0,dropdown:!1,align:"center",...this.pagination},sortable:!1}},items(){return this.models.map(this.item)},multiple(){return!0===this.options.multiple&&1!==this.options.max}},watch:{search(){this.updateSearch()}},created(){this.updateSearch=dt(this.updateSearch,200)},methods:{async fetch(){const t={page:this.pagination.page,search:this.search,...this.fetchData||{}};try{const e=await this.$api.get(this.options.endpoint,t);this.models=e.data,this.pagination=e.pagination,this.onFetched&&this.onFetched(e)}catch(e){this.models=[],this.issue=e.message}},async open(t,e){this.pagination.page=0,this.search=null;let s=!0;Array.isArray(t)?(this.models=t,s=!1):(this.models=[],e=t),this.options={...this.options,...e},this.selected={},this.options.selected.forEach((t=>{this.$set(this.selected,t,{id:t})})),s&&await this.fetch(),this.$refs.dialog.open()},paginate(t){this.pagination.page=t.page,this.pagination.limit=t.limit,this.fetch()},submit(){this.$emit("submit",Object.values(this.selected)),this.$refs.dialog.close()},isSelected(t){return void 0!==this.selected[t.id]},item:t=>t,toggle(t){!1!==this.options.multiple&&1!==this.options.max||(this.selected={}),!0!==this.isSelected(t)?this.options.max&&this.options.max<=Object.keys(this.selected).length||this.$set(this.selected,t.id,t):this.$delete(this.selected,t.id)},toggleBtn(t){const e=this.isSelected(t);return{icon:e?this.checkedIcon:"circle-outline",tooltip:e?this.$t("remove"):this.$t("select"),theme:e?"positive":null}},updateSearch(){this.pagination.page=0,this.fetch()}}};const Bt=It({mixins:[Dt],computed:{emptyProps(){return{icon:"image",text:this.$t("dialog.files.empty")}}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",staticClass:"k-files-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[e("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.options.search?e("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),e("k-collection",t._b({on:{item:t.toggle,paginate:t.paginate},scopedSlots:t._u([{key:"options",fn:function({item:s}){return[e("k-button",t._b({on:{click:function(e){return t.toggle(s)}}},"k-button",t.toggleBtn(s),!1))]}}])},"k-collection",t.collection,!1))]],2)}),[],!1,null,null,null,null).exports;const Pt=It({mixins:[Et],props:{fields:{type:[Array,Object],default:()=>[]},novalidate:{type:Boolean,default:!0},size:{type:String,default:"medium"},submitButton:{type:[String,Boolean],default:()=>window.panel.$t("save")},text:{type:String},theme:{type:String,default:"positive"},value:{type:Object,default:()=>({})}},data(){return{model:this.value}},computed:{hasFields(){return Object.keys(this.fields).length>0}},watch:{value(t,e){t!==e&&(this.model=t)}}},(function(){var t=this,e=t._self._c;return e("k-dialog",t._b({ref:"dialog",on:{cancel:function(e){return t.$emit("cancel")},close:function(e){return t.$emit("close")},ready:function(e){return t.$emit("ready")},submit:function(e){return t.$refs.form.submit()}}},"k-dialog",t.$props,!1),[t.text?[e("k-text",{attrs:{html:t.text}})]:t._e(),t.hasFields?e("k-form",{ref:"form",attrs:{value:t.model,fields:t.fields,novalidate:t.novalidate},on:{input:function(e){return t.$emit("input",e)},submit:function(e){return t.$emit("submit",e)}}}):e("k-box",{attrs:{theme:"negative"}},[t._v(" This form dialog has no fields ")])],2)}),[],!1,null,null,null,null).exports;const Nt=It({extends:Pt,watch:{"model.name"(t){this.fields.code.disabled||this.onNameChanges(t)},"model.code"(t){this.fields.code.disabled||(this.model.code=this.$helper.slug(t,[this.$system.ascii]),this.onCodeChanges(this.model.code))}},methods:{onCodeChanges(t){if(!t)return this.model.locale=null;if(t.length>=2)if(-1!==t.indexOf("-")){let e=t.split("-"),s=[e[0],e[1].toUpperCase()];this.model.locale=s.join("_")}else{let e=this.$system.locales||[];(null==e?void 0:e[t])?this.model.locale=e[t]:this.model.locale=null}},onNameChanges(t){this.model.code=this.$helper.slug(t,[this.model.rules,this.$system.ascii]).substr(0,2)}}},null,null,!1,null,null,null,null).exports;const qt=It({mixins:[Dt],data(){const t=Dt.data();return{...t,model:{title:null,parent:null},options:{...t.options,parent:null}}},computed:{emptyProps(){return{icon:"page",text:this.$t("dialog.pages.empty")}},fetchData(){return{parent:this.options.parent}}},methods:{back(){this.options.parent=this.model.parent,this.pagination.page=1,this.fetch()},go(t){this.options.parent=t.id,this.pagination.page=1,this.fetch()},onFetched(t){this.model=t.model}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",staticClass:"k-pages-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[e("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.model?e("header",{staticClass:"k-pages-dialog-navbar"},[e("k-button",{attrs:{disabled:!t.model.id,tooltip:t.$t("back"),icon:"angle-left"},on:{click:t.back}}),e("k-headline",[t._v(t._s(t.model.title))])],1):t._e(),t.options.search?e("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),e("k-collection",t._b({on:{item:t.toggle,paginate:t.paginate},scopedSlots:t._u([{key:"options",fn:function({item:s}){return[e("k-button",t._b({on:{click:function(e){return t.toggle(s)}}},"k-button",t.toggleBtn(s),!1)),t.model?e("k-button",{attrs:{disabled:!s.hasChildren,tooltip:t.$t("open"),icon:"angle-right"},on:{click:function(e){return e.stopPropagation(),t.go(s)}}}):t._e()]}}])},"k-collection",t.collection,!1))]],2)}),[],!1,null,null,null,null).exports;const Ft=It({mixins:[Et],props:{icon:{type:String,default:"trash"},submitButton:{type:[String,Boolean],default:()=>window.panel.$t("delete")},text:String,theme:{type:String,default:"negative"}}},(function(){var t=this;return(0,t._self._c)("k-text-dialog",t._g(t._b({ref:"dialog"},"k-text-dialog",t.$props,!1),t.$listeners),[t._t("default")],2)}),[],!1,null,null,null,null).exports;const Rt=It({mixins:[Et],props:{text:String}},(function(){var t=this,e=t._self._c;return e("k-dialog",t._g(t._b({ref:"dialog"},"k-dialog",t.$props,!1),t.$listeners),[t._t("default",(function(){return[t.text?e("k-text",{attrs:{html:t.text}}):e("k-box",{attrs:{theme:"negative"}},[t._v(" This dialog does not define any text ")])]}))],2)}),[],!1,null,null,null,null).exports;const zt=It({mixins:[Dt],computed:{emptyProps(){return{icon:"users",text:this.$t("dialog.users.empty")}}},methods:{item:t=>({...t,key:t.email,info:t.info!==t.text?t.info:null})}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",staticClass:"k-users-dialog",attrs:{size:"medium"},on:{cancel:function(e){return t.$emit("cancel")},submit:t.submit}},[t.issue?[e("k-box",{attrs:{text:t.issue,theme:"negative"}})]:[t.options.search?e("k-input",{staticClass:"k-dialog-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text",icon:"search"},model:{value:t.search,callback:function(e){t.search=e},expression:"search"}}):t._e(),e("k-collection",t._b({on:{item:t.toggle,paginate:t.paginate},scopedSlots:t._u([{key:"options",fn:function({item:s}){return[e("k-button",t._b({on:{click:function(e){return t.toggle(s)}}},"k-button",t.toggleBtn(s),!1))]}}])},"k-collection",t.collection,!1))]],2)}),[],!1,null,null,null,null).exports;const Yt=It({inheritAttrs:!1,props:{id:String,icon:String,tab:String,tabs:Object,title:String},data:()=>({click:!1}),computed:{breadcrumb(){return this.$store.state.drawers.open},hasTabs(){return this.tabs&&Object.keys(this.tabs).length>1},index(){return this.breadcrumb.findIndex((t=>t.id===this._uid))},nested(){return this.index>0}},watch:{index(){-1===this.index&&this.close()}},destroyed(){this.$store.dispatch("drawers/close",this._uid)},methods:{close(){this.$refs.overlay.close()},goTo(t){if(t===this._uid)return!0;this.$store.dispatch("drawers/goto",t)},mouseup(){!0===this.click&&this.close(),this.click=!1},mousedown(t=!1){this.click=t,!0===this.click&&this.$store.dispatch("drawers/close")},onClose(){this.$store.dispatch("drawers/close",this._uid),this.$emit("close")},onOpen(){this.$store.dispatch("drawers/open",{id:this._uid,icon:this.icon,title:this.title}),this.$emit("open")},open(){this.$refs.overlay.open()}}},(function(){var t=this,e=t._self._c;return e("k-overlay",{ref:"overlay",attrs:{dimmed:!1},on:{close:t.onClose,open:t.onOpen}},[e("div",{staticClass:"k-drawer",attrs:{"data-id":t.id,"data-nested":t.nested},on:{mousedown:function(e){return e.stopPropagation(),t.mousedown(!0)},mouseup:t.mouseup}},[e("div",{staticClass:"k-drawer-box",on:{mousedown:function(e){return e.stopPropagation(),t.mousedown(!1)}}},[e("header",{staticClass:"k-drawer-header"},[1===t.breadcrumb.length?e("h2",{staticClass:"k-drawer-title"},[e("k-icon",{attrs:{type:t.icon}}),t._v(" "+t._s(t.title)+" ")],1):e("ul",{staticClass:"k-drawer-breadcrumb"},t._l(t.breadcrumb,(function(s){return e("li",{key:s.id},[e("k-button",{attrs:{icon:s.icon,text:s.title},on:{click:function(e){return t.goTo(s.id)}}})],1)})),0),t.hasTabs?e("nav",{staticClass:"k-drawer-tabs"},t._l(t.tabs,(function(s){return e("k-button",{key:s.name,staticClass:"k-drawer-tab",attrs:{current:t.tab==s.name,text:s.label},on:{click:function(e){return e.stopPropagation(),t.$emit("tab",s.name)}}})})),1):t._e(),e("nav",{staticClass:"k-drawer-options"},[t._t("options"),e("k-button",{staticClass:"k-drawer-option",attrs:{icon:"check"},on:{click:t.close}})],2)]),e("div",{staticClass:"k-drawer-body scroll-y-auto"},[t._t("default")],2)])])])}),[],!1,null,null,null,null).exports;const Ht=It({inheritAttrs:!1,props:{empty:{type:String,default:()=>"Missing field setup"},icon:String,id:String,tabs:Object,title:String,type:String,value:Object},data:()=>({tab:null}),computed:{fields(){const t=this.tab||null;return(this.tabs[t]||this.firstTab).fields||{}},firstTab(){return Object.values(this.tabs)[0]}},methods:{close(){this.$refs.drawer.close()},focus(t){var e;"function"==typeof(null==(e=this.$refs.form)?void 0:e.focus)&&this.$refs.form.focus(t)},open(t,e=!0){this.$refs.drawer.open(),this.tab=t||this.firstTab.name,!1!==e&&setTimeout((()=>{let t=Object.values(this.fields).filter((t=>!0===t.autofocus))[0]||null;this.focus(t)}),1)}}},(function(){var t=this,e=t._self._c;return e("k-drawer",{ref:"drawer",staticClass:"k-form-drawer",attrs:{id:t.id,icon:t.icon,tabs:t.tabs,tab:t.tab,title:t.title},on:{close:function(e){return t.$emit("close")},open:function(e){return t.$emit("open")},tab:function(e){t.tab=e}},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options")]},proxy:!0},{key:"default",fn:function(){return[0===Object.keys(t.fields).length?e("k-box",{attrs:{theme:"info"}},[t._v(" "+t._s(t.empty)+" ")]):e("k-form",{ref:"form",attrs:{autofocus:!0,fields:t.fields,value:t.$helper.clone(t.value)},on:{input:function(e){return t.$emit("input",e)}}})]},proxy:!0}],null,!0)})}),[],!1,null,null,null,null).exports;const Ut=It({props:{html:{type:Boolean,default:!1},limit:{type:Number,default:10},skip:{type:Array,default:()=>[]},options:Array,query:String},data:()=>({matches:[],selected:{text:null}}),methods:{close(){this.$refs.dropdown.close()},onSelect(t){this.$emit("select",t),this.$refs.dropdown.close()},search(t){if(t.length<1)return;const e=new RegExp(RegExp.escape(t),"ig");this.matches=this.options.filter((t=>!!t.text&&(-1===this.skip.indexOf(t.value)&&null!==t.text.match(e)))).slice(0,this.limit),this.$emit("search",t,this.matches),this.$refs.dropdown.open()}}},(function(){var t=this,e=t._self._c;return e("k-dropdown",{staticClass:"k-autocomplete"},[t._t("default"),e("k-dropdown-content",t._g({ref:"dropdown",attrs:{autofocus:!0}},t.$listeners),t._l(t.matches,(function(s,n){return e("k-dropdown-item",t._b({key:n,on:{mousedown:function(e){return t.onSelect(s)},keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:(e.preventDefault(),t.onSelect(s))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.preventDefault(),t.onSelect(s))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])||"button"in e&&0!==e.button?null:(e.preventDefault(),t.close.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)?null:(e.preventDefault(),t.close.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.close.apply(null,arguments))}]}},"k-dropdown-item",s,!1),[e("span",{domProps:{innerHTML:t._s(t.html?s.text:t.$esc(s.text))}})])})),1),t._v(" "+t._s(t.query)+" ")],2)}),[],!1,null,null,null,null).exports;const Kt=It({props:{disabled:Boolean,max:String,min:String,value:String},data(){return this.data(this.value)},computed:{numberOfDays(){return this.toDate().daysInMonth()},firstWeekday(){const t=this.toDate().day();return t>0?t:7},weekdays(){return["mon","tue","wed","thu","fri","sat","sun"].map((t=>this.$t("days."+t)))},weeks(){const t=this.firstWeekday-1;return Math.ceil((this.numberOfDays+t)/7)},monthnames(){return["january","february","march","april","may","june","july","august","september","october","november","december"].map((t=>this.$t("months."+t)))},months(){var t=[];return this.monthnames.forEach(((e,s)=>{const n=this.toDate(1,s);t.push({value:s,text:e,disabled:n.isBefore(this.current.min,"month")||n.isAfter(this.current.max,"month")})})),t},years(){var t,e,s,n;const i=null!=(e=null==(t=this.current.min)?void 0:t.get("year"))?e:this.current.year-20,o=null!=(n=null==(s=this.current.max)?void 0:s.get("year"))?n:this.current.year+20;return this.toOptions(i,o)}},watch:{value(t){const e=this.data(t);this.dt=e.dt,this.current=e.current}},methods:{data(t){const e=this.$library.dayjs.iso(t),s=this.$library.dayjs();return{dt:e,current:{month:(null!=e?e:s).month(),year:(null!=e?e:s).year(),min:this.$library.dayjs.iso(this.min),max:this.$library.dayjs.iso(this.max)}}},days(t){let e=[];const s=7*(t-1)+1,n=s+7;for(let i=s;ithis.numberOfDays;e.push(s?"":t)}return e},isDisabled(t){const e=this.toDate(t);return this.disabled||e.isBefore(this.current.min,"day")||e.isAfter(this.current.max,"day")},isSelected(t){return this.toDate(t).isSame(this.dt,"day")},isToday(t){const e=this.$library.dayjs();return this.toDate(t).isSame(e,"day")},onInput(){var t;this.$emit("input",(null==(t=this.dt)?void 0:t.toISO("date"))||null)},onNext(){const t=this.toDate().add(1,"month");this.show(t)},onPrev(){const t=this.toDate().subtract(1,"month");this.show(t)},select(t){const e="today"===t?this.$library.dayjs().merge(this.toDate(),"time"):this.toDate(t);this.dt=e,this.show(e),this.onInput()},show(t){this.current.year=t.year(),this.current.month=t.month()},toDate(t=1,e=this.current.month){return this.$library.dayjs(`${this.current.year}-${e+1}-${t}`)},toOptions(t,e){for(var s=[],n=t;n<=e;n++)s.push({value:n,text:this.$helper.pad(n)});return s}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-calendar-input"},[e("nav",[e("k-button",{attrs:{icon:"angle-left"},on:{click:t.onPrev}}),e("span",{staticClass:"k-calendar-selects"},[e("k-select-input",{attrs:{options:t.months,disabled:t.disabled,required:!0},model:{value:t.current.month,callback:function(e){t.$set(t.current,"month",t._n(e))},expression:"current.month"}}),e("k-select-input",{attrs:{options:t.years,disabled:t.disabled,required:!0},model:{value:t.current.year,callback:function(e){t.$set(t.current,"year",t._n(e))},expression:"current.year"}})],1),e("k-button",{attrs:{icon:"angle-right"},on:{click:t.onNext}})],1),e("table",{staticClass:"k-calendar-table"},[e("thead",[e("tr",t._l(t.weekdays,(function(s){return e("th",{key:"weekday_"+s},[t._v(" "+t._s(s)+" ")])})),0)]),e("tbody",t._l(t.weeks,(function(s){return e("tr",{key:"week_"+s},t._l(t.days(s),(function(s,n){return e("td",{key:"day_"+n,staticClass:"k-calendar-day",attrs:{"aria-current":!!t.isToday(s)&&"date","aria-selected":!!t.isSelected(s)&&"date"}},[s?e("k-button",{attrs:{disabled:t.isDisabled(s),text:s},on:{click:function(e){return t.select(s)}}}):t._e()],1)})),0)})),0),e("tfoot",[e("tr",[e("td",{staticClass:"k-calendar-today",attrs:{colspan:"7"}},[e("k-button",{attrs:{text:t.$t("today")},on:{click:function(e){return t.select("today")}}})],1)])])])])}),[],!1,null,null,null,null).exports;const Gt=It({props:{count:Number,min:Number,max:Number,required:{type:Boolean,default:!1}},computed:{valid(){return!1===this.required&&0===this.count||(!0!==this.required||0!==this.count)&&(!(this.min&&this.countthis.max))}}},(function(){var t=this,e=t._self._c;return e("span",{staticClass:"k-counter",attrs:{"data-invalid":!t.valid}},[e("span",[t._v(t._s(t.count))]),t.min&&t.max?e("span",{staticClass:"k-counter-rules"},[t._v("("+t._s(t.min)+"–"+t._s(t.max)+")")]):t.min?e("span",{staticClass:"k-counter-rules"},[t._v("≥ "+t._s(t.min))]):t.max?e("span",{staticClass:"k-counter-rules"},[t._v("≤ "+t._s(t.max))]):t._e()])}),[],!1,null,null,null,null).exports;const Jt=It({props:{disabled:Boolean,config:Object,fields:{type:[Array,Object],default:()=>({})},novalidate:{type:Boolean,default:!1},value:{type:Object,default:()=>({})}},data(){return{errors:{},listeners:{...this.$listeners,submit:this.onSubmit}}},methods:{focus(t){var e,s;null==(s=null==(e=this.$refs.fields)?void 0:e.focus)||s.call(e,t)},onSubmit(){this.$emit("submit",this.value)},submit(){this.$refs.submitter.click()}}},(function(){var t=this,e=t._self._c;return e("form",{ref:"form",staticClass:"k-form",attrs:{method:"POST",autocomplete:"off",novalidate:""},on:{submit:function(e){return e.preventDefault(),t.onSubmit.apply(null,arguments)}}},[t._t("header"),t._t("default",(function(){return[e("k-fieldset",t._g({ref:"fields",attrs:{disabled:t.disabled,fields:t.fields,novalidate:t.novalidate},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}},t.listeners))]})),t._t("footer"),e("input",{ref:"submitter",staticClass:"k-form-submitter",attrs:{type:"submit"}})],2)}),[],!1,null,null,null,null).exports;const Vt=It({props:{lock:[Boolean,Object]},data:()=>({isRefreshing:null,isLocking:null}),computed:{hasChanges(){return this.$store.getters["content/hasChanges"]()},isDisabled(){return!1===this.$store.state.content.status.enabled},isLocked(){return"lock"===this.lockState},isUnlocked(){return"unlock"===this.lockState},mode(){return null!==this.lockState?this.lockState:!0===this.hasChanges?"changes":null},lockState(){return this.supportsLocking&&this.lock?this.lock.state:null},supportsLocking(){return!1!==this.lock},theme(){return"lock"===this.mode?"negative":"unlock"===this.mode?"info":"notice"}},watch:{hasChanges:{handler(t,e){!0===this.supportsLocking&&!1===this.isLocked&&!1===this.isUnlocked&&(!0===t?(this.onLock(),this.isLocking=setInterval(this.onLock,3e4)):e&&(clearInterval(this.isLocking),this.onLock(!1)))},immediate:!0},isLocked(t){!1===t&&this.$events.$emit("model.reload")}},created(){this.supportsLocking&&(this.isRefreshing=setInterval(this.check,1e4)),this.$events.$on("keydown.cmd.s",this.onSave)},destroyed(){clearInterval(this.isRefreshing),clearInterval(this.isLocking),this.$events.$off("keydown.cmd.s",this.onSave)},methods:{check(){this.$reload({navigate:!1,only:"$view.props.lock",silent:!0})},async onLock(t=!0){const e=[this.$view.path+"/lock",null,null,!0];if(!0===t)try{await this.$api.patch(...e)}catch(s){clearInterval(this.isLocking),this.$store.dispatch("content/revert")}else clearInterval(this.isLocking),await this.$api.delete(...e)},onDownload(){let t="";const e=this.$store.getters["content/changes"]();Object.keys(e).forEach((s=>{t+=s+": \n\n"+e[s],t+="\n\n----\n\n"}));let s=document.createElement("a");s.setAttribute("href","data:text/plain;charset=utf-8,"+encodeURIComponent(t)),s.setAttribute("download",this.$view.path+".txt"),s.style.display="none",document.body.appendChild(s),s.click(),document.body.removeChild(s)},async onResolve(){await this.onUnlock(!1),this.$store.dispatch("content/revert")},onRevert(){this.$refs.revert.open()},async onSave(t){if(!t)return!1;t.preventDefault&&t.preventDefault();try{await this.$store.dispatch("content/save"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")}catch(e){if(403===e.code)return;e.details&&Object.keys(e.details).length>0?this.$store.dispatch("notification/error",{message:this.$t("error.form.incomplete"),details:e.details}):this.$store.dispatch("notification/error",{message:this.$t("error.form.notSaved"),details:[{label:"Exception: "+e.exception,message:e.message}]})}},async onUnlock(t=!0){const e=[this.$view.path+"/unlock",null,null,!0];!0===t?await this.$api.patch(...e):await this.$api.delete(...e),this.$reload({silent:!0})},revert(){this.$store.dispatch("content/revert"),this.$refs.revert.close()}}},(function(){var t=this,e=t._self._c;return e("nav",{staticClass:"k-form-buttons",attrs:{"data-theme":t.theme}},["unlock"===t.mode?e("k-view",[e("p",{staticClass:"k-form-lock-info"},[t._v(" "+t._s(t.$t("lock.isUnlocked"))+" ")]),e("span",{staticClass:"k-form-lock-buttons"},[e("k-button",{staticClass:"k-form-button",attrs:{text:t.$t("download"),icon:"download"},on:{click:t.onDownload}}),e("k-button",{staticClass:"k-form-button",attrs:{text:t.$t("confirm"),icon:"check"},on:{click:t.onResolve}})],1)]):"lock"===t.mode?e("k-view",[e("p",{staticClass:"k-form-lock-info"},[e("k-icon",{attrs:{type:"lock"}}),e("span",{domProps:{innerHTML:t._s(t.$t("lock.isLocked",{email:t.$esc(t.lock.data.email)}))}})],1),t.lock.data.unlockable?e("k-button",{staticClass:"k-form-button",attrs:{text:t.$t("lock.unlock"),icon:"unlock"},on:{click:function(e){return t.onUnlock()}}}):e("k-icon",{staticClass:"k-form-lock-loader",attrs:{type:"loader"}})],1):"changes"===t.mode?e("k-view",[e("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,text:t.$t("revert"),icon:"undo"},on:{click:t.onRevert}}),e("k-button",{staticClass:"k-form-button",attrs:{disabled:t.isDisabled,text:t.$t("save"),icon:"check"},on:{click:t.onSave}})],1):t._e(),e("k-dialog",{ref:"revert",attrs:{"submit-button":t.$t("revert"),icon:"undo",theme:"negative"},on:{submit:t.revert}},[e("k-text",{attrs:{html:t.$t("revert.confirm")}})],1)],1)}),[],!1,null,null,null,null).exports;const Wt=It({data:()=>({isOpen:!1,options:[]}),computed:{hasChanges(){return this.ids.length>0},ids(){return Object.keys(this.store).filter((t=>{var e;return Object.keys((null==(e=this.store[t])?void 0:e.changes)||{}).length>0}))},store(){return this.$store.state.content.models}},methods:{async toggle(){if(!1===this.$refs.list.isOpen)try{await this.$dropdown("changes",{method:"POST",body:{ids:this.ids}})((t=>{this.options=t}))}catch(t){return this.$store.dispatch("notification/success",this.$t("lock.unsaved.empty")),this.$store.dispatch("content/clear"),!1}this.$refs.list&&this.$refs.list.toggle()}}},(function(){var t=this,e=t._self._c;return t.hasChanges?e("k-dropdown",{staticClass:"k-form-indicator"},[e("k-button",{staticClass:"k-form-indicator-toggle k-topbar-button",attrs:{icon:"edit"},on:{click:t.toggle}}),e("k-dropdown-content",{ref:"list",attrs:{align:"right",theme:"light"}},[e("p",{staticClass:"k-form-indicator-info"},[t._v(t._s(t.$t("lock.unsaved"))+":")]),e("hr"),t._l(t.options,(function(s){return e("k-dropdown-item",t._b({key:s.id},"k-dropdown-item",s,!1),[t._v(" "+t._s(s.text)+" ")])}))],2)],1):t._e()}),[],!1,null,null,null,null).exports,Xt={props:{after:String}},Zt={props:{autofocus:Boolean}},Qt={props:{before:String}},te={props:{disabled:Boolean}},ee={props:{help:String}},se={props:{id:{type:[Number,String],default(){return this._uid}}}},ne={props:{invalid:Boolean}},ie={props:{label:String}},oe={props:{name:[Number,String]}},re={props:{required:Boolean}},le={mixins:[te,ee,ie,oe,re],props:{counter:[Boolean,Object],endpoints:Object,input:[String,Number],translate:Boolean,type:String}};const ae=It({mixins:[le],inheritAttrs:!1,computed:{labelText(){return this.label||" "}}},(function(){var t=this,e=t._self._c;return e("div",{class:"k-field k-field-name-"+t.name,attrs:{"data-disabled":t.disabled,"data-translate":t.translate},on:{focusin:function(e){return t.$emit("focus",e)},focusout:function(e){return t.$emit("blur",e)}}},[t._t("header",(function(){return[e("header",{staticClass:"k-field-header"},[t._t("label",(function(){return[e("label",{staticClass:"k-field-label",attrs:{for:t.input}},[t._v(" "+t._s(t.labelText)+" "),t.required?e("abbr",{attrs:{title:t.$t("field.required")}},[t._v("*")]):t._e()])]})),t._t("options"),t._t("counter",(function(){return[t.counter?e("k-counter",t._b({staticClass:"k-field-counter",attrs:{required:t.required}},"k-counter",t.counter,!1)):t._e()]}))],2)]})),t._t("default"),t._t("footer",(function(){return[t.help||t.$slots.help?e("footer",{staticClass:"k-field-footer"},[t._t("help",(function(){return[t.help?e("k-text",{staticClass:"k-field-help",attrs:{theme:"help",html:t.help}}):t._e()]}))],2):t._e()]}))],2)}),[],!1,null,null,null,null).exports;const ue=It({props:{config:Object,disabled:Boolean,fields:{type:[Array,Object],default:()=>[]},novalidate:{type:Boolean,default:!1},value:{type:Object,default:()=>({})}},data:()=>({errors:{}}),methods:{focus(t){if(t)return void(this.hasField(t)&&"function"==typeof this.$refs[t][0].focus&&this.$refs[t][0].focus());const e=Object.keys(this.$refs)[0];this.focus(e)},hasFieldType(t){return this.$helper.isComponent(`k-${t}-field`)},hasField(t){var e;return null==(e=this.$refs[t])?void 0:e[0]},meetsCondition(t){if(!t.when)return!0;let e=!0;return Object.keys(t.when).forEach((s=>{this.value[s.toLowerCase()]!==t.when[s]&&(e=!1)})),e},onInvalid(t,e,s,n){this.errors[n]=e,this.$emit("invalid",this.errors)},hasErrors(){return Object.keys(this.errors).length}}},(function(){var t=this,e=t._self._c;return e("fieldset",{staticClass:"k-fieldset"},[e("k-grid",[t._l(t.fields,(function(s,n){return["hidden"!==s.type&&t.meetsCondition(s)?e("k-column",{key:s.signature,attrs:{width:s.width}},[e("k-error-boundary",[t.hasFieldType(s.type)?e("k-"+s.type+"-field",t._b({ref:n,refInFor:!0,tag:"component",attrs:{"form-data":t.value,name:n,novalidate:t.novalidate,disabled:t.disabled||s.disabled},on:{input:function(e){return t.$emit("input",t.value,s,n)},focus:function(e){return t.$emit("focus",e,s,n)},invalid:(e,i)=>t.onInvalid(e,i,s,n),submit:function(e){return t.$emit("submit",e,s,n)}},model:{value:t.value[n],callback:function(e){t.$set(t.value,n,e)},expression:"value[fieldName]"}},"component",s,!1)):e("k-box",{attrs:{theme:"negative"}},[e("k-text",{attrs:{size:"small"}},[t._v(" The field type "),e("strong",[t._v('"'+t._s(n)+'"')]),t._v(" does not exist ")])],1)],1)],1):t._e()]}))],2)],1)}),[],!1,null,null,null,null).exports,ce={mixins:[Xt,Qt,te,ne],props:{autofocus:Boolean,type:String,icon:[String,Boolean],theme:String,novalidate:{type:Boolean,default:!1},value:{type:[String,Boolean,Number,Object,Array],default:null}}};const de=It({mixins:[ce],inheritAttrs:!1,data(){return{isInvalid:this.invalid,listeners:{...this.$listeners,invalid:(t,e)=>{this.isInvalid=t,this.$emit("invalid",t,e)}}}},computed:{inputProps(){return{...this.$props,...this.$attrs}}},methods:{blur(t){(null==t?void 0:t.relatedTarget)&&!1===this.$el.contains(t.relatedTarget)&&this.trigger(null,"blur")},focus(t){this.trigger(t,"focus")},select(t){this.trigger(t,"select")},trigger(t,e){var s,n,i;if("INPUT"===(null==(s=null==t?void 0:t.target)?void 0:s.tagName)&&"function"==typeof(null==(n=null==t?void 0:t.target)?void 0:n[e]))return void t.target[e]();if("function"==typeof(null==(i=this.$refs.input)?void 0:i[e]))return void this.$refs.input[e]();const o=this.$el.querySelector("input, select, textarea");"function"==typeof(null==o?void 0:o[e])&&o[e]()}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-input",attrs:{"data-disabled":t.disabled,"data-invalid":!t.novalidate&&t.isInvalid,"data-theme":t.theme,"data-type":t.type}},[t.$slots.before||t.before?e("span",{staticClass:"k-input-before",on:{click:t.focus}},[t._t("before",(function(){return[t._v(t._s(t.before))]}))],2):t._e(),e("span",{staticClass:"k-input-element",on:{click:function(e){return e.stopPropagation(),t.focus.apply(null,arguments)}}},[t._t("default",(function(){return[e("k-"+t.type+"-input",t._g(t._b({ref:"input",tag:"component",attrs:{value:t.value}},"component",t.inputProps,!1),t.listeners))]}))],2),t.$slots.after||t.after?e("span",{staticClass:"k-input-after",on:{click:t.focus}},[t._t("after",(function(){return[t._v(t._s(t.after))]}))],2):t._e(),t.$slots.icon||t.icon?e("span",{staticClass:"k-input-icon",on:{click:t.focus}},[t._t("icon",(function(){return[e("k-icon",{attrs:{type:t.icon}})]}))],2):t._e()])}),[],!1,null,null,null,null).exports;const pe=It({props:{methods:Array},data:()=>({currentForm:null,isLoading:!1,issue:"",user:{email:"",password:"",remember:!1}}),computed:{canToggle(){return null!==this.codeMode&&!0===this.methods.includes("password")&&(!0===this.methods.includes("password-reset")||!0===this.methods.includes("code"))},codeMode(){return!0===this.methods.includes("password-reset")?"password-reset":!0===this.methods.includes("code")?"code":null},fields(){let t={email:{autofocus:!0,label:this.$t("email"),type:"email",required:!0,link:!1}};return"email-password"===this.form&&(t.password={label:this.$t("password"),type:"password",minLength:8,required:!0,autocomplete:"current-password",counter:!1}),t},form(){return this.currentForm?this.currentForm:"password"===this.methods[0]?"email-password":"email"},isResetForm(){return"password-reset"===this.codeMode&&"email"===this.form},toggleText(){return this.$t("login.toggleText."+this.codeMode+"."+this.formOpposite(this.form))}},methods:{formOpposite:t=>"email-password"===t?"email":"email-password",async login(){this.issue=null,this.isLoading=!0;let t=Object.assign({},this.user);"email"===this.currentForm&&(t.password=null),!0===this.isResetForm&&(t.remember=!1);try{await this.$api.auth.login(t),this.$reload({globals:["$system","$translation"]})}catch(e){this.issue=e.message}finally{this.isLoading=!1}},toggleForm(){this.currentForm=this.formOpposite(this.form),this.$refs.fieldset.focus("email")}}},(function(){var t=this,e=t._self._c;return e("form",{staticClass:"k-login-form",on:{submit:function(e){return e.preventDefault(),t.login.apply(null,arguments)}}},[e("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("login"))+" ")]),t.issue?e("k-login-alert",{on:{click:function(e){t.issue=null}}},[t._v(" "+t._s(t.issue)+" ")]):t._e(),e("div",{staticClass:"k-login-fields"},[!0===t.canToggle?e("button",{staticClass:"k-login-toggler",attrs:{type:"button"},on:{click:t.toggleForm}},[t._v(" "+t._s(t.toggleText)+" ")]):t._e(),e("k-fieldset",{ref:"fieldset",attrs:{novalidate:!0,fields:t.fields},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}})],1),e("div",{staticClass:"k-login-buttons"},[!1===t.isResetForm?e("span",{staticClass:"k-login-checkbox"},[e("k-checkbox-input",{attrs:{value:t.user.remember,label:t.$t("login.remember")},on:{input:function(e){t.user.remember=e}}})],1):t._e(),e("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v(" "+t._s(t.$t("login"+(t.isResetForm?".reset":"")))+" "),t.isLoading?[t._v(" … ")]:t._e()],2)],1)],1)}),[],!1,null,null,null,null).exports;const he=It({props:{methods:Array,pending:Object},data:()=>({code:"",isLoadingBack:!1,isLoadingLogin:!1,issue:""}),computed:{mode(){return!0===this.methods.includes("password-reset")?"password-reset":"login"}},methods:{async back(){this.isLoadingBack=!0,this.$go("/logout")},async login(){this.issue=null,this.isLoadingLogin=!0;try{await this.$api.auth.verifyCode(this.code),this.$store.dispatch("notification/success",this.$t("welcome")),"password-reset"===this.mode?this.$go("reset-password"):this.$reload()}catch(t){this.issue=t.message}finally{this.isLoadingLogin=!1}}}},(function(){var t=this,e=t._self._c;return e("form",{staticClass:"k-login-form k-login-code-form",on:{submit:function(e){return e.preventDefault(),t.login.apply(null,arguments)}}},[e("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("login"))+" ")]),t.issue?e("k-login-alert",{on:{click:function(e){t.issue=null}}},[t._v(" "+t._s(t.issue)+" ")]):t._e(),e("k-user-info",{attrs:{user:t.pending.email}}),e("k-text-field",{attrs:{autofocus:!0,counter:!1,help:t.$t("login.code.text."+t.pending.challenge),label:t.$t("login.code.label."+t.mode),novalidate:!0,placeholder:t.$t("login.code.placeholder."+t.pending.challenge),required:!0,autocomplete:"one-time-code",icon:"unlock",name:"code"},model:{value:t.code,callback:function(e){t.code=e},expression:"code"}}),e("div",{staticClass:"k-login-buttons"},[e("k-button",{staticClass:"k-login-button k-login-back-button",attrs:{icon:"angle-left"},on:{click:t.back}},[t._v(" "+t._s(t.$t("back"))+" "),t.isLoadingBack?[t._v(" … ")]:t._e()],2),e("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v(" "+t._s(t.$t("login"+("password-reset"===t.mode?".reset":"")))+" "),t.isLoadingLogin?[t._v(" … ")]:t._e()],2)],1)],1)}),[],!1,null,null,null,null).exports;const me=It({props:{display:{type:String,default:"HH:mm"},value:String},computed:{day(){return this.formatTimes([6,7,8,9,10,11,"-",12,13,14,15,16,17])},night(){return this.formatTimes([18,19,20,21,22,23,"-",0,1,2,3,4,5])}},methods:{formatTimes(t){return t.map((t=>{if("-"===t)return t;const e=this.$library.dayjs(t+":00","H:mm");return{display:e.format(this.display),select:e.toISO("time")}}))},select(t){this.$emit("input",t)}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-times"},[e("div",{staticClass:"k-times-slot"},[e("k-icon",{attrs:{type:"sun"}}),e("ul",t._l(t.day,(function(s){return e("li",{key:s.select},["-"===s?e("hr"):e("k-button",{on:{click:function(e){return t.select(s.select)}}},[t._v(t._s(s.display))])],1)})),0)],1),e("div",{staticClass:"k-times-slot"},[e("k-icon",{attrs:{type:"moon"}}),e("ul",t._l(t.night,(function(s){return e("li",{key:s.select},["-"===s?e("hr"):e("k-button",{on:{click:function(e){return t.select(s.select)}}},[t._v(t._s(s.display))])],1)})),0)],1)])}),[],!1,null,null,null,null).exports;const fe=It({props:{accept:{type:String,default:"*"},attributes:{type:Object},max:{type:Number},method:{type:String,default:"POST"},multiple:{type:Boolean,default:!0},url:{type:String}},data(){return{options:this.$props,completed:{},errors:[],files:[],total:0}},computed:{limit(){return!1===this.options.multiple?1:this.options.max}},methods:{open(t){this.params(t),setTimeout((()=>{this.$refs.input.click()}),1)},params(t){this.options=Object.assign({},this.$props,t)},select(t){this.upload(t.target.files)},drop(t,e){this.params(e),this.upload(t)},upload(t){this.$refs.dialog.open(),this.files=[...t],this.completed={},this.errors=[],this.hasErrors=!1,this.limit&&(this.files=this.files.slice(0,this.limit)),this.total=this.files.length,this.files.forEach((t=>{var e,s;this.$helper.upload(t,{url:this.options.url,attributes:this.options.attributes,method:this.options.method,headers:{"X-CSRF":window.panel.$system.csrf},progress:(t,e,s)=>{var n,i;null==(i=null==(n=this.$refs[e.name])?void 0:n[0])||i.set(s)},success:(t,e,s)=>{this.complete(e,s.data)},error:(t,e,s)=>{this.errors.push({file:e,message:s.message}),this.complete(e,s.data)}}),void 0!==(null==(s=null==(e=this.options)?void 0:e.attributes)?void 0:s.sort)&&this.options.attributes.sort++}))},complete(t,e){if(this.completed[t.name]=e,Object.keys(this.completed).length==this.total){if(this.$refs.input.value="",this.errors.length>0)return this.$forceUpdate(),void this.$emit("error",this.files);setTimeout((()=>{this.$refs.dialog.close(),this.$emit("success",this.files,Object.values(this.completed))}),250)}}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-upload"},[e("input",{ref:"input",attrs:{accept:t.options.accept,multiple:t.options.multiple,"aria-hidden":"true",type:"file",tabindex:"-1"},on:{change:t.select,click:function(t){t.stopPropagation()}}}),e("k-dialog",{ref:"dialog",staticClass:"k-upload-dialog",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"},scopedSlots:t._u([{key:"footer",fn:function(){return[t.errors.length>0?[e("k-button-group",{attrs:{buttons:[{icon:"check",text:t.$t("confirm"),click:()=>t.$refs.dialog.close()}]}})]:t._e()]},proxy:!0}])},[t.errors.length>0?[e("k-headline",[t._v(t._s(t.$t("upload.errors")))]),e("ul",{staticClass:"k-upload-error-list"},t._l(t.errors,(function(s,n){return e("li",{key:"error-"+n},[e("p",{staticClass:"k-upload-error-filename"},[t._v(" "+t._s(s.file.name)+" ")]),e("p",{staticClass:"k-upload-error-message"},[t._v(" "+t._s(s.message)+" ")])])})),0)]:[e("k-headline",[t._v(t._s(t.$t("upload.progress")))]),e("ul",{staticClass:"k-upload-list"},t._l(t.files,(function(s,n){return e("li",{key:"file-"+n},[e("k-progress",{ref:s.name,refInFor:!0}),e("p",{staticClass:"k-upload-list-filename"},[t._v(" "+t._s(s.name)+" ")]),e("p",[t._v(t._s(t.errors[s.name]))])],1)})),0)]],2)],1)}),[],!1,null,null,null,null).exports;const ge=t=>({$from:e})=>((t,e)=>{for(let s=t.depth;s>0;s--){const n=t.node(s);if(e(n))return{pos:s>0?t.before(s):0,start:t.start(s),depth:s,node:n}}})(e,t),ke=t=>e=>{if((t=>t instanceof c)(e)){const{node:s,$from:n}=e;if(((t,e)=>Array.isArray(t)&&t.indexOf(e.type)>-1||e.type===t)(t,s))return{node:s,pos:n.pos,depth:n.depth}}},be=(t,e,s={})=>{const n=ke(e)(t.selection)||ge((t=>t.type===e))(t.selection);return Object.keys(s).length&&n?n.node.hasMarkup(e,{...n.node.attrs,...s}):!!n};function ye(t=null,e=null){if(!t||!e)return!1;const s=t.parent.childAfter(t.parentOffset);if(!s.node)return!1;const n=s.node.marks.find((t=>t.type===e));if(!n)return!1;let i=t.index(),o=t.start()+s.offset,r=i+1,l=o+s.node.nodeSize;for(;i>0&&n.isInSet(t.parent.child(i-1).marks);)i-=1,o-=t.parent.child(i).nodeSize;for(;r{i=[...i,...t.marks]}));const o=i.find((t=>t.type.name===e.name));return o?o.attrs:{}},getNodeAttrs:function(t,e){const{from:s,to:n}=t.selection;let i=[];t.doc.nodesBetween(s,n,(t=>{i=[...i,t]}));const o=i.reverse().find((t=>t.type.name===e.name));return o?o.attrs:{}},markInputRule:function(t,e,s){return new r(t,((t,n,i,o)=>{const r=s instanceof Function?s(n):s,{tr:l}=t,a=n.length-1;let u=o,c=i;if(n[a]){const s=i+n[0].indexOf(n[a-1]),r=s+n[a-1].length-1,d=s+n[a-1].lastIndexOf(n[a]),p=d+n[a].length,h=function(t,e,s){let n=[];return s.doc.nodesBetween(t,e,((t,e)=>{n=[...n,...t.marks.map((s=>({start:e,end:e+t.nodeSize,mark:s})))]})),n}(i,o,t).filter((t=>{const{excluded:s}=t.mark.type;return s.find((t=>t.name===e.name))})).filter((t=>t.end>s));if(h.length)return!1;ps&&l.delete(s,d),c=s,u=c+n[a].length}return l.addMark(c,u,e.create(r)),l.removeStoredMark(e),l}))},markIsActive:function(t,e){const{from:s,$from:n,to:i,empty:o}=t.selection;return o?!!e.isInSet(t.storedMarks||n.marks()):!!t.doc.rangeHasMark(s,i,e)},markPasteRule:function(t,e,s){const n=(i,o)=>{const r=[];return i.forEach((i=>{var l;if(i.isText){const{text:n,marks:a}=i;let u,c=0;const d=!!a.filter((t=>"link"===t.type.name))[0];for(;!d&&null!==(u=t.exec(n));)if((null==(l=null==o?void 0:o.type)?void 0:l.allowsMarkType(e))&&u[1]){const t=u.index,n=t+u[0].length,o=t+u[0].indexOf(u[1]),l=o+u[1].length,a=s instanceof Function?s(u):s;t>0&&r.push(i.cut(c,t)),r.push(i.cut(o,l).mark(e.create(a).addToSet(i.marks))),c=n}cnew a(n(t.content),t.openStart,t.openEnd)}})},minMax:function(t=0,e=0,s=0){return Math.min(Math.max(parseInt(t,10),e),s)},nodeIsActive:be,nodeInputRule:function(t,e,s){return new r(t,((t,n,i,o)=>{const r=s instanceof Function?s(n):s,{tr:l}=t;return n[0]&&l.replaceWith(i-1,o,e.create(r)),l}))},pasteRule:function(t,e,s){const n=i=>{const o=[];return i.forEach((i=>{if(i.isText){const{text:n}=i;let r,l=0;do{if(r=t.exec(n),r){const t=r.index,n=t+r[0].length,a=s instanceof Function?s(r[0]):s;t>0&&o.push(i.cut(l,t)),o.push(i.cut(t,n).mark(e.create(a).addToSet(i.marks))),l=n}}while(r);lnew a(n(t.content),t.openStart,t.openEnd)}})},removeMark:function(t){return(e,s)=>{const{tr:n,selection:i}=e;let{from:o,to:r}=i;const{$from:l,empty:a}=i;if(a){const e=ye(l,t);o=e.from,r=e.to}return n.removeMark(o,r,t),s(n)}},toggleBlockType:function(t,e,s={}){return(n,i,o)=>be(n,t,s)?d(e)(n,i,o):d(t,s)(n,i,o)},toggleList:function(t,e){return(s,n,i)=>{const{schema:o,selection:r}=s,{$from:l,$to:a}=r,u=l.blockRange(a);if(!u)return!1;const c=ge((t=>ve(t,o)))(r);if(u.depth>=1&&c&&u.depth-c.depth<=1){if(c.node.type===t)return p(e)(s,n,i);if(ve(c.node,o)&&t.validContent(c.node.content)){const{tr:e}=s;return e.setNodeMarkup(c.pos,t),n&&n(e),!1}}return h(t)(s,n,i)}},updateMark:function(t,e){return(s,n)=>{const{tr:i,selection:o,doc:r}=s,{ranges:l,empty:a}=o;if(a){const{from:s,to:n}=ye(o.$from,t);r.rangeHasMark(s,n,t)&&i.removeMark(s,n,t),i.addMark(s,n,t.create(e))}else l.forEach((s=>{const{$to:n,$from:o}=s;r.rangeHasMark(o.pos,n.pos,t)&&i.removeMark(o.pos,n.pos,t),i.addMark(o.pos,n.pos,t.create(e))}));return n(i)}}};class _e{constructor(t=[],e){t.forEach((t=>{t.bindEditor(e),t.init()})),this.extensions=t}commands({schema:t,view:e}){return this.extensions.filter((t=>t.commands)).reduce(((s,n)=>{const{name:i,type:o}=n,r={},l=n.commands({schema:t,utils:$e,...["node","mark"].includes(o)?{type:t[`${o}s`][i]}:{}}),a=(t,s)=>{r[t]=t=>{if("function"!=typeof s||!e.editable)return!1;e.focus();const n=s(t);return"function"==typeof n?n(e.state,e.dispatch,e):n}};return"object"==typeof l?Object.entries(l).forEach((([t,e])=>{a(t,e)})):a(i,l),{...s,...r}}),{})}buttons(t="mark"){const e={};return this.extensions.filter((e=>e.type===t)).filter((t=>t.button)).forEach((t=>{Array.isArray(t.button)?t.button.forEach((t=>{e[t.id||t.name]=t})):e[t.name]=t.button})),e}getAllowedExtensions(t){return t instanceof Array||!t?t instanceof Array?this.extensions.filter((e=>!t.includes(e.name))):this.extensions:[]}getFromExtensions(t,e,s=this.extensions){return s.filter((t=>["extension"].includes(t.type))).filter((e=>e[t])).map((s=>s[t]({...e,utils:$e})))}getFromNodesAndMarks(t,e,s=this.extensions){return s.filter((t=>["node","mark"].includes(t.type))).filter((e=>e[t])).map((s=>s[t]({...e,type:e.schema[`${s.type}s`][s.name],utils:$e})))}inputRules({schema:t,excludedExtensions:e}){const s=this.getAllowedExtensions(e);return[...this.getFromExtensions("inputRules",{schema:t},s),...this.getFromNodesAndMarks("inputRules",{schema:t},s)].reduce(((t,e)=>[...t,...e]),[])}keymaps({schema:t}){return[...this.getFromExtensions("keys",{schema:t}),...this.getFromNodesAndMarks("keys",{schema:t})].map((t=>_(t)))}get marks(){return this.extensions.filter((t=>"mark"===t.type)).reduce(((t,{name:e,schema:s})=>({...t,[e]:s})),{})}get nodes(){return this.extensions.filter((t=>"node"===t.type)).reduce(((t,{name:e,schema:s})=>({...t,[e]:s})),{})}get options(){const{view:t}=this;return this.extensions.reduce(((e,s)=>({...e,[s.name]:new Proxy(s.options,{set(e,s,n){const i=e[s]!==n;return Object.assign(e,{[s]:n}),i&&t.updateState(t.state),!0}})})),{})}pasteRules({schema:t,excludedExtensions:e}){const s=this.getAllowedExtensions(e);return[...this.getFromExtensions("pasteRules",{schema:t},s),...this.getFromNodesAndMarks("pasteRules",{schema:t},s)].reduce(((t,e)=>[...t,...e]),[])}plugins({schema:t}){return[...this.getFromExtensions("plugins",{schema:t}),...this.getFromNodesAndMarks("plugins",{schema:t})].reduce(((t,e)=>[...t,...e]),[]).map((t=>t instanceof l?t:new l(t)))}}class xe{constructor(t={}){this.options={...this.defaults,...t}}init(){return null}bindEditor(t=null){this.editor=t}get name(){return null}get type(){return"extension"}get defaults(){return{}}plugins(){return[]}inputRules(){return[]}pasteRules(){return[]}keys(){return{}}}class we extends xe{constructor(t={}){super(t)}get type(){return"node"}get schema(){return null}commands(){return{}}}class Se extends we{get defaults(){return{inline:!1}}get name(){return"doc"}get schema(){return{content:this.options.inline?"inline*":"block+"}}}class Ce extends we{get button(){return{id:this.name,icon:"paragraph",label:window.panel.$t("toolbar.button.paragraph"),name:this.name}}commands({utils:t,type:e}){return{paragraph:()=>t.setBlockType(e)}}get schema(){return{content:"inline*",group:"block",draggable:!1,parseDOM:[{tag:"p"}],toDOM:()=>["p",0]}}get name(){return"paragraph"}}class Oe extends we{get name(){return"text"}get schema(){return{group:"inline"}}}class Ae extends class{emit(t,...e){this._callbacks=this._callbacks||{};const s=this._callbacks[t];return s&&s.forEach((t=>t.apply(this,e))),this}off(t,e){if(arguments.length){const s=this._callbacks?this._callbacks[t]:null;s&&(e?this._callbacks[t]=s.filter((t=>t!==e)):delete this._callbacks[t])}else this._callbacks={};return this}on(t,e){return this._callbacks=this._callbacks||{},this._callbacks[t]=this._callbacks[t]||[],this._callbacks[t].push(e),this}}{constructor(t={}){super(),this.defaults={autofocus:!1,content:"",disableInputRules:!1,disablePasteRules:!1,editable:!0,element:null,extensions:[],emptyDocument:{type:"doc",content:[]},events:{},inline:!1,parseOptions:{},topNode:"doc",useBuiltInExtensions:!0},this.init(t)}blur(){this.view.dom.blur()}get builtInExtensions(){return this.options.useBuiltInExtensions?[new Se({inline:this.options.inline}),new Oe,new Ce]:[]}buttons(t){return this.extensions.buttons(t)}clearContent(t=!1){this.setContent(this.options.emptyDocument,t)}command(t,...e){this.commands[t]&&this.commands[t](...e)}createCommands(){return this.extensions.commands({schema:this.schema,view:this.view})}createDocument(t,e=this.options.parseOptions){if(null===t)return this.schema.nodeFromJSON(this.options.emptyDocument);if("object"==typeof t)try{return this.schema.nodeFromJSON(t)}catch(s){return window.console.warn("Invalid content.","Passed value:",t,"Error:",s),this.schema.nodeFromJSON(this.options.emptyDocument)}if("string"==typeof t){const s=`

${t}
`,n=(new window.DOMParser).parseFromString(s,"text/html").body.firstElementChild;return x.fromSchema(this.schema).parse(n,e)}return!1}createEvents(){const t=this.options.events||{};return Object.entries(t).forEach((([t,e])=>{this.on(t,e)})),t}createExtensions(){return new _e([...this.builtInExtensions,...this.options.extensions],this)}createFocusEvents(){const t=(t,e,s=!0)=>{this.focused=s,this.emit(s?"focus":"blur",{event:e,state:t.state,view:t});const n=this.state.tr.setMeta("focused",s);this.view.dispatch(n)};return new l({props:{attributes:{tabindex:0},handleDOMEvents:{focus:(e,s)=>{t(e,s,!0)},blur:(e,s)=>{t(e,s,!1)}}}})}createInputRules(){return this.extensions.inputRules({schema:this.schema,excludedExtensions:this.options.disableInputRules})}createKeymaps(){return this.extensions.keymaps({schema:this.schema})}createMarks(){return this.extensions.marks}createNodes(){return this.extensions.nodes}createPasteRules(){return this.extensions.pasteRules({schema:this.schema,excludedExtensions:this.options.disablePasteRules})}createPlugins(){return this.extensions.plugins({schema:this.schema})}createSchema(){return new w({topNode:this.options.topNode,nodes:this.nodes,marks:this.marks})}createState(){return S.create({schema:this.schema,doc:this.createDocument(this.options.content),plugins:[...this.plugins,C({rules:this.inputRules}),...this.pasteRules,...this.keymaps,_({Backspace:I}),_(M),this.createFocusEvents()]})}createView(){return new O(this.element,{dispatchTransaction:this.dispatchTransaction.bind(this),editable:()=>this.options.editable,handlePaste:(t,e)=>{if("function"==typeof this.events.paste){const t=e.clipboardData.getData("text/html"),s=e.clipboardData.getData("text/plain");if(!0===this.events.paste(e,t,s))return!0}},handleDrop:(...t)=>{this.emit("drop",...t)},state:this.createState()})}destroy(){this.view&&this.view.destroy()}dispatchTransaction(t){const e=this.state,s=this.state.apply(t);this.view.updateState(s),this.selection={from:this.state.selection.from,to:this.state.selection.to},this.setActiveNodesAndMarks();const n={editor:this,getHTML:this.getHTML.bind(this),getJSON:this.getJSON.bind(this),state:this.state,transaction:t};this.emit("transaction",n),!t.docChanged&&t.getMeta("preventUpdate")||this.emit("update",n);const{from:i,to:o}=this.state.selection,r=!e||!e.selection.eq(s.selection);this.emit(s.selection.empty?"deselect":"select",{...n,from:i,hasChanged:r,to:o})}focus(t=null){if(this.view.focused&&null===t||!1===t)return;const{from:e,to:s}=this.selectionAtPosition(t);this.setSelection(e,s),setTimeout((()=>this.view.focus()),10)}getHTML(){const t=document.createElement("div"),e=A.fromSchema(this.schema).serializeFragment(this.state.doc.content);return t.appendChild(e),this.options.inline&&t.querySelector("p")?t.querySelector("p").innerHTML:t.innerHTML}getJSON(){return this.state.doc.toJSON()}getMarkAttrs(t=null){return this.activeMarkAttrs[t]}getSchemaJSON(){return JSON.parse(JSON.stringify({nodes:this.nodes,marks:this.marks}))}init(t={}){this.options={...this.defaults,...t},this.element=this.options.element,this.focused=!1,this.selection={from:0,to:0},this.events=this.createEvents(),this.extensions=this.createExtensions(),this.nodes=this.createNodes(),this.marks=this.createMarks(),this.schema=this.createSchema(),this.keymaps=this.createKeymaps(),this.inputRules=this.createInputRules(),this.pasteRules=this.createPasteRules(),this.plugins=this.createPlugins(),this.view=this.createView(),this.commands=this.createCommands(),this.setActiveNodesAndMarks(),!1!==this.options.autofocus&&this.focus(this.options.autofocus),this.emit("init",{view:this.view,state:this.state}),this.extensions.view=this.view,this.setContent(this.options.content,!0)}isEditable(){return this.options.editable}isEmpty(){if(this.state)return 0===this.state.doc.textContent.length}get isActive(){return Object.entries({...this.activeMarks,...this.activeNodes}).reduce(((t,[e,s])=>({...t,[e]:(t={})=>s(t)})),{})}removeMark(t){if(this.schema.marks[t])return $e.removeMark(this.schema.marks[t])(this.state,this.view.dispatch)}selectionAtPosition(t=null){if(this.selection&&null===t)return this.selection;if("start"===t||!0===t)return{from:0,to:0};if("end"===t){const{doc:t}=this.state;return{from:t.content.size,to:t.content.size}}return{from:t,to:t}}setActiveNodesAndMarks(){this.activeMarks=Object.values(this.schema.marks).filter((t=>$e.markIsActive(this.state,t))).map((t=>t.name)),this.activeMarkAttrs=Object.entries(this.schema.marks).reduce(((t,[e,s])=>({...t,[e]:$e.getMarkAttrs(this.state,s)})),{}),this.activeNodes=Object.values(this.schema.nodes).filter((t=>$e.nodeIsActive(this.state,t))).map((t=>t.name)),this.activeNodeAttrs=Object.entries(this.schema.nodes).reduce(((t,[e,s])=>({...t,[e]:$e.getNodeAttrs(this.state,s)})),{})}setContent(t={},e=!1,s){const{doc:n,tr:i}=this.state,o=this.createDocument(t,s),r=i.replaceWith(0,n.content.size,o).setMeta("preventUpdate",!e);this.view.dispatch(r)}setSelection(t=0,e=0){const{doc:s,tr:n}=this.state,i=$e.minMax(t,0,s.content.size),o=$e.minMax(e,0,s.content.size),r=T.create(s,i,o),l=n.setSelection(r);this.view.dispatch(l)}get state(){return this.view?this.view.state:null}toggleMark(t){if(this.schema.marks[t])return $e.toggleMark(this.schema.marks[t])(this.state,this.view.dispatch)}updateMark(t,e){if(this.schema.marks[t])return $e.updateMark(this.schema.marks[t],e)(this.state,this.view.dispatch)}}const Te=It({data:()=>({link:{href:null,title:null,target:!1}}),computed:{fields(){return{href:{label:this.$t("url"),type:"text",icon:"url"},title:{label:this.$t("title"),type:"text",icon:"title"},target:{label:this.$t("open.newWindow"),type:"toggle",text:[this.$t("no"),this.$t("yes")]}}}},methods:{open(t){this.link={title:null,target:!1,...t},this.link.target=Boolean(this.link.target),this.$refs.dialog.open()},submit(){this.$emit("submit",{...this.link,target:this.link.target?"_blank":null}),this.$refs.dialog.close()}}},(function(){var t=this;return(0,t._self._c)("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("confirm"),size:"medium"},on:{close:function(e){return t.$emit("close")},submit:t.submit},model:{value:t.link,callback:function(e){t.link=e},expression:"link"}})}),[],!1,null,null,null,null).exports;const Ie=It({data:()=>({email:{email:null,title:null}}),computed:{fields(){return{href:{label:this.$t("email"),type:"email",icon:"email"},title:{label:this.$t("title"),type:"text",icon:"title"}}}},methods:{open(t){this.email={title:null,...t},this.$refs.dialog.open()},submit(){this.$emit("submit",this.email),this.$refs.dialog.close()}}},(function(){var t=this;return(0,t._self._c)("k-form-dialog",{ref:"dialog",attrs:{fields:t.fields,"submit-button":t.$t("confirm"),size:"medium"},on:{close:function(e){return t.$emit("close")},submit:t.submit},model:{value:t.email,callback:function(e){t.email=e},expression:"email"}})}),[],!1,null,null,null,null).exports;class Me extends xe{constructor(t={}){super(t)}command(){return()=>{}}remove(){this.editor.removeMark(this.name)}get schema(){return null}get type(){return"mark"}toggle(){return this.editor.toggleMark(this.name)}update(t){this.editor.updateMark(this.name,t)}}class Ee extends Me{get button(){return{icon:"code",label:window.panel.$t("toolbar.button.code")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/(?:`)([^`]+)(?:`)$/,t)]}keys(){return{"Mod-`":()=>this.toggle()}}get name(){return"code"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/(?:`)([^`]+)(?:`)/g,t)]}get schema(){return{excludes:"_",parseDOM:[{tag:"code"}],toDOM:()=>["code",0]}}}class Le extends Me{get button(){return{icon:"bold",label:window.panel.$t("toolbar.button.bold")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)$/,t)]}keys(){return{"Mod-b":()=>this.toggle()}}get name(){return"bold"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/(?:\*\*|__)([^*_]+)(?:\*\*|__)/g,t)]}get schema(){return{parseDOM:[{tag:"strong"},{tag:"b",getAttrs:t=>"normal"!==t.style.fontWeight&&null},{style:"font-weight",getAttrs:t=>/^(bold(er)?|[5-9]\d{2,})$/.test(t)&&null}],toDOM:()=>["strong",0]}}}class je extends Me{get button(){return{icon:"italic",label:window.panel.$t("toolbar.button.italic")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/(?:^|\s)((?:\*)((?:[^*]+))(?:\*))$/,t),e.markInputRule(/(?:^|\s)((?:_)((?:[^_]+))(?:_))$/,t)]}keys(){return{"Mod-i":()=>this.toggle()}}get name(){return"italic"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/_([^_]+)_/g,t),e.markPasteRule(/\*([^*]+)\*/g,t)]}get schema(){return{parseDOM:[{tag:"i"},{tag:"em"},{style:"font-style=italic"}],toDOM:()=>["em",0]}}}class De extends Me{get button(){return{icon:"url",label:window.panel.$t("toolbar.button.link")}}commands(){return{link:()=>{this.editor.emit("link",this.editor)},insertLink:(t={})=>{if(t.href)return this.update(t)},removeLink:()=>this.remove(),toggleLink:(t={})=>{var e;(null==(e=t.href)?void 0:e.length)>0?this.editor.command("insertLink",t):this.editor.command("removeLink")}}}get defaults(){return{target:null}}get name(){return"link"}pasteRules({type:t,utils:e}){return[e.pasteRule(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z]{2,}\b([-a-zA-Z0-9@:%_+.~#?&//=,]*)/gi,t,(t=>({href:t})))]}plugins(){return[{props:{handleClick:(t,e,s)=>{const n=this.editor.getMarkAttrs("link");n.href&&!0===s.altKey&&s.target instanceof HTMLAnchorElement&&(s.stopPropagation(),window.open(n.href,n.target))}}}]}get schema(){return{attrs:{href:{default:null},target:{default:null},title:{default:null}},inclusive:!1,parseDOM:[{tag:"a[href]:not([href^='mailto:'])",getAttrs:t=>({href:t.getAttribute("href"),target:t.getAttribute("target"),title:t.getAttribute("title")})}],toDOM:t=>["a",{...t.attrs,rel:"noopener noreferrer"},0]}}}class Be extends Me{get button(){return{icon:"email",label:"Email"}}commands(){return{email:()=>{this.editor.emit("email")},insertEmail:(t={})=>{if(t.href)return this.update(t)},removeEmail:()=>this.remove(),toggleEmail:(t={})=>{var e;(null==(e=t.href)?void 0:e.length)>0?this.editor.command("insertEmail",t):this.editor.command("removeEmail")}}}get defaults(){return{target:null}}get name(){return"email"}pasteRules({type:t,utils:e}){return[e.pasteRule(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/gi,t,(t=>({href:t})))]}plugins(){return[{props:{handleClick:(t,e,s)=>{const n=this.editor.getMarkAttrs("email");n.href&&!0===s.altKey&&s.target instanceof HTMLAnchorElement&&(s.stopPropagation(),window.open(n.href))}}}]}get schema(){return{attrs:{href:{default:null},title:{default:null}},inclusive:!1,parseDOM:[{tag:"a[href^='mailto:']",getAttrs:t=>({href:t.getAttribute("href").replace("mailto:",""),title:t.getAttribute("title")})}],toDOM:t=>["a",{...t.attrs,href:"mailto:"+t.attrs.href},0]}}}class Pe extends Me{get button(){return{icon:"strikethrough",label:window.panel.$t("toolbar.button.strike")}}commands(){return()=>this.toggle()}inputRules({type:t,utils:e}){return[e.markInputRule(/~([^~]+)~$/,t)]}keys(){return{"Mod-d":()=>this.toggle()}}get name(){return"strike"}pasteRules({type:t,utils:e}){return[e.markPasteRule(/~([^~]+)~/g,t)]}get schema(){return{parseDOM:[{tag:"s"},{tag:"del"},{tag:"strike"},{style:"text-decoration",getAttrs:t=>"line-through"===t}],toDOM:()=>["s",0]}}}class Ne extends Me{get button(){return{icon:"underline",label:window.panel.$t("toolbar.button.underline")}}commands(){return()=>this.toggle()}keys(){return{"Mod-u":()=>this.toggle()}}get name(){return"underline"}get schema(){return{parseDOM:[{tag:"u"},{style:"text-decoration",getAttrs:t=>"underline"===t}],toDOM:()=>["u",0]}}}class qe extends we{get button(){return{id:this.name,icon:"list-bullet",label:window.panel.$t("toolbar.button.ul"),name:this.name,when:["listItem","bulletList","orderedList"]}}commands({type:t,schema:e,utils:s}){return()=>s.toggleList(t,e.nodes.listItem)}inputRules({type:t,utils:e}){return[e.wrappingInputRule(/^\s*([-+*])\s$/,t)]}keys({type:t,schema:e,utils:s}){return{"Shift-Ctrl-8":s.toggleList(t,e.nodes.listItem)}}get name(){return"bulletList"}get schema(){return{content:"listItem+",group:"block",parseDOM:[{tag:"ul"}],toDOM:()=>["ul",0]}}}class Fe extends we{commands({utils:t,type:e}){return()=>this.createHardBreak(t,e)}createHardBreak(t,e){return t.chainCommands(t.exitCode,((t,s)=>(s(t.tr.replaceSelectionWith(e.create()).scrollIntoView()),!0)))}get defaults(){return{enter:!1,text:!1}}keys({utils:t,type:e}){const s=this.createHardBreak(t,e);let n={"Mod-Enter":s,"Shift-Enter":s};return this.options.enter&&(n.Enter=s),n}get name(){return"hardBreak"}get schema(){return{inline:!0,group:"inline",selectable:!1,parseDOM:[{tag:"br"}],toDOM:()=>["br"]}}}class Re extends we{get button(){return this.options.levels.map((t=>({id:`h${t}`,command:`h${t}`,icon:`h${t}`,label:window.panel.$t("toolbar.button.heading."+t),attrs:{level:t},name:this.name,when:["heading","paragraph"]})))}commands({type:t,schema:e,utils:s}){let n={toggleHeading:n=>s.toggleBlockType(t,e.nodes.paragraph,n)};return this.options.levels.forEach((i=>{n[`h${i}`]=()=>s.toggleBlockType(t,e.nodes.paragraph,{level:i})})),n}get defaults(){return{levels:[1,2,3,4,5,6]}}inputRules({type:t,utils:e}){return this.options.levels.map((s=>e.textblockTypeInputRule(new RegExp(`^(#{1,${s}})\\s$`),t,(()=>({level:s})))))}keys({type:t,utils:e}){return this.options.levels.reduce(((s,n)=>({...s,[`Shift-Ctrl-${n}`]:e.setBlockType(t,{level:n})})),{})}get name(){return"heading"}get schema(){return{attrs:{level:{default:1}},content:"inline*",group:"block",defining:!0,draggable:!1,parseDOM:this.options.levels.map((t=>({tag:`h${t}`,attrs:{level:t}}))),toDOM:t=>[`h${t.attrs.level}`,0]}}}class ze extends we{commands({type:t}){return()=>(e,s)=>s(e.tr.replaceSelectionWith(t.create()))}inputRules({type:t,utils:e}){return[e.nodeInputRule(/^(?:---|___\s|\*\*\*\s)$/,t)]}get name(){return"horizontalRule"}get schema(){return{group:"block",parseDOM:[{tag:"hr"}],toDOM:()=>["hr"]}}}class Ye extends we{keys({type:t,utils:e}){return{Enter:e.splitListItem(t),"Shift-Tab":e.liftListItem(t),Tab:e.sinkListItem(t)}}get name(){return"listItem"}get schema(){return{content:"paragraph block*",defining:!0,draggable:!1,parseDOM:[{tag:"li"}],toDOM:()=>["li",0]}}}class He extends we{get button(){return{id:this.name,icon:"list-numbers",label:window.panel.$t("toolbar.button.ol"),name:this.name,when:["listItem","bulletList","orderedList"]}}commands({type:t,schema:e,utils:s}){return()=>s.toggleList(t,e.nodes.listItem)}inputRules({type:t,utils:e}){return[e.wrappingInputRule(/^(\d+)\.\s$/,t,(t=>({order:+t[1]})),((t,e)=>e.childCount+e.attrs.order===+t[1]))]}keys({type:t,schema:e,utils:s}){return{"Shift-Ctrl-9":s.toggleList(t,e.nodes.listItem)}}get name(){return"orderedList"}get schema(){return{attrs:{order:{default:1}},content:"listItem+",group:"block",parseDOM:[{tag:"ol",getAttrs:t=>({order:t.hasAttribute("start")?+t.getAttribute("start"):1})}],toDOM:t=>1===t.attrs.order?["ol",0]:["ol",{start:t.attrs.order},0]}}}class Ue extends xe{commands(){return{undo:()=>E,redo:()=>L,undoDepth:()=>j,redoDepth:()=>D}}get defaults(){return{depth:"",newGroupDelay:""}}keys(){return{"Mod-z":E,"Mod-y":L,"Shift-Mod-z":L,"Mod-я":E,"Shift-Mod-я":L}}get name(){return"history"}plugins(){return[B({depth:this.options.depth,newGroupDelay:this.options.newGroupDelay})]}}class Ke extends xe{commands(){return{insertHtml:t=>(e,s)=>{let n=document.createElement("div");n.innerHTML=t.trim();const i=x.fromSchema(e.schema).parse(n);s(e.tr.replaceSelectionWith(i).scrollIntoView())}}}}class Ge extends xe{constructor(t={}){super(t)}close(){this.visible=!1,this.emit()}emit(){this.editor.emit("toolbar",{marks:this.marks,nodes:this.nodes,nodeAttrs:this.nodeAttrs,position:this.position,visible:this.visible})}init(){this.position={left:0,bottom:0},this.visible=!1,this.editor.on("blur",(()=>{this.close()})),this.editor.on("deselect",(()=>{this.close()})),this.editor.on("select",(({hasChanged:t})=>{!1!==t?this.open():this.emit()}))}get marks(){return this.editor.activeMarks}get nodes(){return this.editor.activeNodes}get nodeAttrs(){return this.editor.activeNodeAttrs}open(){this.visible=!0,this.reposition(),this.emit()}reposition(){const{from:t,to:e}=this.editor.selection,s=this.editor.view.coordsAtPos(t),n=this.editor.view.coordsAtPos(e,!0),i=this.editor.element.getBoundingClientRect();let o=(s.left+n.left)/2-i.left,r=Math.round(i.bottom-s.top);return this.position={bottom:r,left:o}}get type(){return"toolbar"}}const Je=It({props:{activeMarks:{type:Array,default:()=>[]},activeNodes:{type:Array,default:()=>[]},activeNodeAttrs:{type:[Array,Object],default:()=>[]},editor:{type:Object,required:!0},marks:{type:Array},isParagraphNodeHidden:{type:Boolean,default:!1}},computed:{activeButton(){return Object.values(this.nodeButtons).find((t=>this.isButtonActive(t)))||!1},hasVisibleButtons(){const t=Object.keys(this.nodeButtons);return t.length>1||1===t.length&&!1===t.includes("paragraph")},markButtons(){return this.buttons("mark")},nodeButtons(){let t=this.buttons("node");return!0===this.isParagraphNodeHidden&&t.paragraph&&delete t.paragraph,t}},methods:{buttons(t){const e=this.editor.buttons(t);let s=this.sorting;!1!==s&&!1!==Array.isArray(s)||(s=Object.keys(e));let n={};return s.forEach((t=>{e[t]&&(n[t]=e[t])})),n},command(t,...e){this.$emit("command",t,...e)},isButtonActive(t){if("paragraph"===t.name)return 1===this.activeNodes.length&&this.activeNodes.includes(t.name);let e=!0;if(t.attrs){const s=Object.values(this.activeNodeAttrs).find((e=>JSON.stringify(e)===JSON.stringify(t.attrs)));e=Boolean(s||!1)}return!0===e&&this.activeNodes.includes(t.name)},isButtonCurrent(t){return!!this.activeButton&&this.activeButton.id===t.id},isButtonDisabled(t){var e;if(null==(e=this.activeButton)?void 0:e.when){return!1===this.activeButton.when.includes(t.name)}return!1},needDividerAfterNode(t){let e=["paragraph"],s=Object.keys(this.nodeButtons);return(s.includes("bulletList")||s.includes("orderedList"))&&e.push("h6"),e.includes(t.id)}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-writer-toolbar"},[t.hasVisibleButtons?e("k-dropdown",{nativeOn:{mousedown:function(t){t.preventDefault()}}},[e("k-button",{class:{"k-writer-toolbar-button k-writer-toolbar-nodes":!0,"k-writer-toolbar-button-active":!!t.activeButton},attrs:{icon:t.activeButton.icon||"title"},on:{click:function(e){return t.$refs.nodes.toggle()}}}),e("k-dropdown-content",{ref:"nodes"},[t._l(t.nodeButtons,(function(s,n){return[e("k-dropdown-item",{key:n,attrs:{current:t.isButtonCurrent(s),disabled:t.isButtonDisabled(s),icon:s.icon},on:{click:function(e){return t.command(s.command||n)}}},[t._v(" "+t._s(s.label)+" ")]),t.needDividerAfterNode(s)?e("hr",{key:n+"-divider"}):t._e()]}))],2)],1):t._e(),t._l(t.markButtons,(function(s,n){return e("k-button",{key:n,class:{"k-writer-toolbar-button":!0,"k-writer-toolbar-button-active":t.activeMarks.includes(n)},attrs:{icon:s.icon,tooltip:s.label},on:{mousedown:function(e){return e.preventDefault(),t.command(s.command||n)}}})}))],2)}),[],!1,null,null,null,null).exports,Ve={props:{autofocus:Boolean,breaks:Boolean,code:Boolean,disabled:Boolean,emptyDocument:{type:Object,default:()=>({type:"doc",content:[]})},headings:[Array,Boolean],inline:{type:Boolean,default:!1},marks:{type:[Array,Boolean],default:!0},nodes:{type:[Array,Boolean],default:()=>["heading","bulletList","orderedList"]},paste:{type:Function,default:()=>()=>!1},placeholder:String,spellcheck:Boolean,extensions:Array,value:{type:String,default:""}}};const We=It({components:{"k-writer-email-dialog":Ie,"k-writer-link-dialog":Te,"k-writer-toolbar":Je},mixins:[Ve],data(){return{editor:null,json:{},html:this.value,isEmpty:!0,toolbar:!1}},computed:{isParagraphNodeHidden(){return!0===Array.isArray(this.nodes)&&3!==this.nodes.length&&!1===this.nodes.includes("paragraph")}},watch:{value(t,e){t!==e&&t!==this.html&&(this.html=t,this.editor.setContent(this.html))}},mounted(){this.editor=new Ae({autofocus:this.autofocus,content:this.value,editable:!this.disabled,element:this.$el,emptyDocument:this.emptyDocument,events:{link:t=>{this.$refs.linkDialog.open(t.getMarkAttrs("link"))},email:()=>{this.$refs.emailDialog.open(this.editor.getMarkAttrs("email"))},paste:this.paste,toolbar:t=>{this.toolbar=t,this.toolbar.visible&&this.$nextTick((()=>{this.onToolbarOpen()}))},update:t=>{if(!this.editor)return;const e=JSON.stringify(this.editor.getJSON());e!==JSON.stringify(this.json)&&(this.json=e,this.isEmpty=t.editor.isEmpty(),this.html=t.editor.getHTML(),this.isEmpty&&(0===t.editor.activeNodes.length||t.editor.activeNodes.includes("paragraph"))&&(this.html=""),this.$emit("input",this.html))}},extensions:[...this.createMarks(),...this.createNodes(),new Ue,new Ke,new Ge,...this.extensions||[]],inline:this.inline}),this.isEmpty=this.editor.isEmpty(),this.json=this.editor.getJSON()},beforeDestroy(){this.editor.destroy()},methods:{filterExtensions(t,e,s){!1===e?e=[]:!0!==e&&!1!==Array.isArray(e)||(e=Object.keys(t));let n=[];return e.forEach((e=>{t[e]&&n.push(t[e])})),"function"==typeof s&&(n=s(e,n)),n},command(t,...e){this.editor.command(t,...e)},createMarks(){return this.filterExtensions({bold:new Le,italic:new je,strike:new Pe,underline:new Ne,code:new Ee,link:new De,email:new Be},this.marks)},createNodes(){const t=new Fe({text:!0,enter:this.inline});return!0===this.inline?[t]:this.filterExtensions({bulletList:new qe,orderedList:new He,heading:new Re,horizontalRule:new ze,listItem:new Ye},this.nodes,((e,s)=>((e.includes("bulletList")||e.includes("orderedList"))&&s.push(new Ye),s.push(t),s)))},getHTML(){return this.editor.getHTML()},focus(){this.editor.focus()},onToolbarOpen(){if(this.$refs.toolbar){const t=this.$el.clientWidth,e=this.$refs.toolbar.$el.clientWidth;let s=this.toolbar.position.left;s-e/2<0&&(s=s+(e/2-s)-20),s+e/2>t&&(s=s-(s+e/2-t)+20),s!==this.toolbar.position.left&&(this.$refs.toolbar.$el.style.left=s+"px")}}}},(function(){var t=this,e=t._self._c;return e("div",{directives:[{name:"direction",rawName:"v-direction"}],ref:"editor",staticClass:"k-writer",attrs:{"data-empty":t.isEmpty,"data-placeholder":t.placeholder,spellcheck:t.spellcheck}},[t.editor?[t.toolbar.visible?e("k-writer-toolbar",{ref:"toolbar",style:{bottom:t.toolbar.position.bottom+"px","inset-inline-start":t.toolbar.position.left+"px"},attrs:{editor:t.editor,"active-marks":t.toolbar.marks,"active-nodes":t.toolbar.nodes,"active-node-attrs":t.toolbar.nodeAttrs,"is-paragraph-node-hidden":t.isParagraphNodeHidden},on:{command:function(e){return t.editor.command(e)}}}):t._e(),e("k-writer-link-dialog",{ref:"linkDialog",on:{close:function(e){return t.editor.focus()},submit:function(e){return t.editor.command("toggleLink",e)}}}),e("k-writer-email-dialog",{ref:"emailDialog",on:{close:function(e){return t.editor.focus()},submit:function(e){return t.editor.command("toggleEmail",e)}}})]:t._e()],2)}),[],!1,null,null,null,null).exports;const Xe=It({},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-login-alert",on:{click:function(e){return t.$emit("click")}}},[e("span",[t._t("default")],2),e("k-icon",{attrs:{type:"alert"}})],1)}),[],!1,null,null,null,null).exports;const Ze=It({props:{fields:Object,index:[Number,String],total:Number,value:Object},mounted(){this.$store.dispatch("content/disable"),this.$events.$on("keydown.cmd.s",this.onSubmit),this.$events.$on("keydown.esc",this.onDiscard)},destroyed(){this.$events.$off("keydown.cmd.s",this.onSubmit),this.$events.$off("keydown.esc",this.onDiscard),this.$store.dispatch("content/enable")},methods:{focus(t){this.$refs.form.focus(t)},onDiscard(){this.$emit("discard")},onInput(t){this.$emit("input",t)},onSubmit(){this.$emit("submit")}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-structure-form"},[e("div",{staticClass:"k-structure-backdrop",on:{click:t.onDiscard}}),e("section",[e("k-form",{ref:"form",staticClass:"k-structure-form-fields",attrs:{value:t.value,fields:t.fields},on:{input:t.onInput,submit:t.onSubmit}}),e("footer",{staticClass:"k-structure-form-buttons"},[e("k-button",{staticClass:"k-structure-form-cancel-button",attrs:{text:t.$t("cancel"),icon:"cancel"},on:{click:function(e){return t.$emit("close")}}}),"new"!==t.index?e("k-pagination",{attrs:{dropdown:!1,total:t.total,limit:1,page:t.index+1,details:!0},on:{paginate:function(e){return t.$emit("paginate",e)}}}):t._e(),e("k-button",{staticClass:"k-structure-form-submit-button",attrs:{text:t.$t("new"!==t.index?"confirm":"add"),icon:"check"},on:{click:t.onSubmit}})],1)],1)])}),[],!1,null,null,null,null).exports,Qe=function(t){this.command("insert",((e,s)=>{let n=[];return s.split("\n").forEach(((e,s)=>{let i="ol"===t?s+1+".":"-";n.push(i+" "+e)})),n.join("\n")}))};const ts=It({layout:["headlines","bold","italic","|","link","email","file","|","code","ul","ol"],props:{buttons:{type:[Boolean,Array],default:!0},uploads:[Boolean,Object,Array]},data(){let t={},e={},s=[],n=this.commands();return!1===this.buttons?t:(Array.isArray(this.buttons)&&(s=this.buttons),!0!==Array.isArray(this.buttons)&&(s=this.$options.layout),s.forEach(((s,i)=>{if("|"===s)t["divider-"+i]={divider:!0};else if(n[s]){let i=n[s];t[s]=i,i.shortcut&&(e[i.shortcut]=s)}})),{layout:t,shortcuts:e})},methods:{command(t,e){"function"==typeof t?t.apply(this):this.$emit("command",t,e)},close(){Object.keys(this.$refs).forEach((t=>{const e=this.$refs[t][0];"function"==typeof(null==e?void 0:e.close)&&e.close()}))},fileCommandSetup(){let t={label:this.$t("toolbar.button.file"),icon:"attachment"};return!1===this.uploads?t.command="selectFile":t.dropdown={select:{label:this.$t("toolbar.button.file.select"),icon:"check",command:"selectFile"},upload:{label:this.$t("toolbar.button.file.upload"),icon:"upload",command:"uploadFile"}},t},commands(){return{headlines:{label:this.$t("toolbar.button.headings"),icon:"title",dropdown:{h1:{label:this.$t("toolbar.button.heading.1"),icon:"title",command:"prepend",args:"#"},h2:{label:this.$t("toolbar.button.heading.2"),icon:"title",command:"prepend",args:"##"},h3:{label:this.$t("toolbar.button.heading.3"),icon:"title",command:"prepend",args:"###"}}},bold:{label:this.$t("toolbar.button.bold"),icon:"bold",command:"wrap",args:"**",shortcut:"b"},italic:{label:this.$t("toolbar.button.italic"),icon:"italic",command:"wrap",args:"*",shortcut:"i"},link:{label:this.$t("toolbar.button.link"),icon:"url",shortcut:"k",command:"dialog",args:"link"},email:{label:this.$t("toolbar.button.email"),icon:"email",shortcut:"e",command:"dialog",args:"email"},file:this.fileCommandSetup(),code:{label:this.$t("toolbar.button.code"),icon:"code",command:"wrap",args:"`"},ul:{label:this.$t("toolbar.button.ul"),icon:"list-bullet",command(){return Qe.apply(this,["ul"])}},ol:{label:this.$t("toolbar.button.ol"),icon:"list-numbers",command(){return Qe.apply(this,["ol"])}}}},shortcut(t,e){if(this.shortcuts[t]){const s=this.layout[this.shortcuts[t]];if(!s)return!1;e.preventDefault(),this.command(s.command,s.args)}}}},(function(){var t=this,e=t._self._c;return e("nav",{staticClass:"k-toolbar"},[e("div",{staticClass:"k-toolbar-wrapper"},[e("div",{staticClass:"k-toolbar-buttons"},[t._l(t.layout,(function(s,n){return[s.divider?[e("span",{key:n,staticClass:"k-toolbar-divider"})]:s.dropdown?[e("k-dropdown",{key:n},[e("k-button",{key:n,staticClass:"k-toolbar-button",attrs:{icon:s.icon,tooltip:s.label,tabindex:"-1"},on:{click:function(e){t.$refs[n+"-dropdown"][0].toggle()}}}),e("k-dropdown-content",{ref:n+"-dropdown",refInFor:!0},t._l(s.dropdown,(function(s,n){return e("k-dropdown-item",{key:n,attrs:{icon:s.icon},on:{click:function(e){return t.command(s.command,s.args)}}},[t._v(" "+t._s(s.label)+" ")])})),1)],1)]:[e("k-button",{key:n,staticClass:"k-toolbar-button",attrs:{icon:s.icon,tooltip:s.label,tabindex:"-1"},on:{click:function(e){return t.command(s.command,s.args)}}})]]}))],2)])])}),[],!1,null,null,null,null).exports;const es=It({data(){return{value:{email:null,text:null},fields:{email:{label:this.$t("email"),type:"email"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext(){return this.$config.kirbytext}},methods:{open(t,e){this.value.text=e,this.$refs.dialog.open()},cancel(){this.$emit("cancel")},createKirbytext(){var t;const e=this.value.email||"";return(null==(t=this.value.text)?void 0:t.length)>0?`(email: ${e} text: ${this.value.text})`:`(email: ${e})`},createMarkdown(){var t;const e=this.value.email||"";return(null==(t=this.value.text)?void 0:t.length)>0?`[${this.value.text}](mailto:${e})`:`<${e}>`},submit(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={email:null,text:null},this.$refs.dialog.close()}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("insert")},on:{close:t.cancel,submit:function(e){return t.$refs.form.submit()}}},[e("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)}),[],!1,null,null,null,null).exports;const ss=It({data(){return{value:{url:null,text:null},fields:{url:{label:this.$t("link"),type:"text",placeholder:this.$t("url.placeholder"),icon:"url"},text:{label:this.$t("link.text"),type:"text"}}}},computed:{kirbytext(){return this.$config.kirbytext}},methods:{open(t,e){this.value.text=e,this.$refs.dialog.open()},cancel(){this.$emit("cancel")},createKirbytext(){return this.value.text.length>0?`(link: ${this.value.url} text: ${this.value.text})`:`(link: ${this.value.url})`},createMarkdown(){return this.value.text.length>0?`[${this.value.text}](${this.value.url})`:`<${this.value.url}>`},submit(){this.$emit("submit",this.kirbytext?this.createKirbytext():this.createMarkdown()),this.value={url:null,text:null},this.$refs.dialog.close()}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",attrs:{"submit-button":t.$t("insert")},on:{close:t.cancel,submit:function(e){return t.$refs.form.submit()}}},[e("k-form",{ref:"form",attrs:{fields:t.fields},on:{submit:t.submit},model:{value:t.value,callback:function(e){t.value=e},expression:"value"}})],1)}),[],!1,null,null,null,null).exports;const ns=It({mixins:[Zt,te,se,ie,re],inheritAttrs:!1,props:{value:Boolean},watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},onChange(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()}},validations(){return{value:{required:!this.required||P.required}}}},(function(){var t=this,e=t._self._c;return e("label",{staticClass:"k-checkbox-input",on:{click:function(t){t.stopPropagation()}}},[e("input",{ref:"input",staticClass:"k-checkbox-input-native input-hidden",attrs:{id:t.id,disabled:t.disabled,type:"checkbox"},domProps:{checked:t.value},on:{change:function(e){return t.onChange(e.target.checked)}}}),e("span",{staticClass:"k-checkbox-input-icon",attrs:{"aria-hidden":"true"}},[e("svg",{attrs:{width:"12",height:"10",viewBox:"0 0 12 10",xmlns:"http://www.w3.org/2000/svg"}},[e("path",{attrs:{d:"M1 5l3.3 3L11 1","stroke-width":"2",fill:"none","fill-rule":"evenodd"}})])]),e("span",{staticClass:"k-checkbox-input-label",domProps:{innerHTML:t._s(t.label)}})])}),[],!1,null,null,null,null).exports,is={mixins:[Zt,te,se,re],props:{columns:Number,max:Number,min:Number,options:Array,value:{type:[Array,Object],default:()=>[]}}};const os=It({mixins:[is],inheritAttrs:!1,data(){return{selected:this.valueToArray(this.value)}},watch:{value(t){this.selected=this.valueToArray(t)},selected(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$el.querySelector("input").focus()},onInput(t,e){if(!0===e)this.selected.push(t);else{const e=this.selected.indexOf(t);-1!==e&&this.selected.splice(e,1)}this.$emit("input",this.selected)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()},valueToArray:t=>!0===Array.isArray(t)?t:"string"==typeof t?String(t).split(","):"object"==typeof t?Object.values(t):void 0},validations(){return{selected:{required:!this.required||P.required,min:!this.min||P.minLength(this.min),max:!this.max||P.maxLength(this.max)}}}},(function(){var t=this,e=t._self._c;return e("ul",{staticClass:"k-checkboxes-input",style:"--columns:"+t.columns},t._l(t.options,(function(s,n){return e("li",{key:n},[e("k-checkbox-input",{attrs:{id:t.id+"-"+n,label:s.text,value:-1!==t.selected.indexOf(s.value)},on:{input:function(e){return t.onInput(s.value,e)}}})],1)})),0)}),[],!1,null,null,null,null).exports,rs={mixins:[Zt,te,se,re],props:{display:{type:String,default:"DD.MM.YYYY"},max:String,min:String,step:{type:Object,default:()=>({size:1,unit:"day"})},type:{type:String,default:"date"},value:String}};const ls=It({mixins:[rs],inheritAttrs:!1,data:()=>({dt:null,formatted:null}),computed:{inputType:()=>"date",pattern(){return this.$library.dayjs.pattern(this.display)},rounding(){return{...this.$options.props.step.default(),...this.step}}},watch:{value:{handler(t,e){if(t!==e){const e=this.toDatetime(t);this.commit(e)}},immediate:!0}},created(){this.$events.$on("keydown.cmd.s",this.onBlur)},destroyed(){this.$events.$off("keydown.cmd.s",this.onBlur)},methods:{alter(t){let e=this.parse()||this.round(this.$library.dayjs()),s=this.rounding.unit,n=this.rounding.size;const i=this.selection();null!==i&&("meridiem"===i.unit?(t="pm"===e.format("a")?"subtract":"add",s="hour",n=12):(s=i.unit,s!==this.rounding.unit&&(n=1))),e=e[t](n,s).round(this.rounding.unit,this.rounding.size),this.commit(e),this.emit(e),this.$nextTick((()=>this.select(i)))},commit(t){this.dt=t,this.formatted=this.pattern.format(t),this.$emit("invalid",this.$v.$invalid,this.$v)},emit(t){this.$emit("input",this.toISO(t))},focus(){this.$refs.input.focus()},onArrowDown(){this.alter("subtract")},onArrowUp(){this.alter("add")},onBlur(){const t=this.parse();this.commit(t),this.emit(t)},onEnter(){this.onBlur(),this.$nextTick((()=>this.$emit("submit")))},onInput(t){const e=this.parse(),s=this.pattern.format(e);if(!t||s==t)return this.commit(e),this.emit(e)},onTab(t){""!=this.$refs.input.value&&(this.onBlur(),this.$nextTick((()=>{const e=this.selection();if(this.$refs.input&&e.start===this.$refs.input.selectionStart&&e.end===this.$refs.input.selectionEnd-1)if(t.shiftKey){if(0===e.index)return;this.selectPrev(e.index)}else{if(e.index===this.pattern.parts.length-1)return;this.selectNext(e.index)}else t.shiftKey?this.selectLast():this.selectFirst();t.preventDefault()})))},parse(){let t=this.$refs.input.value;return t=this.$library.dayjs.interpret(t,this.inputType),this.round(t)},round(t){return(null==t?void 0:t.round(this.rounding.unit,this.rounding.size))||null},select(t){var e;t||(t=this.selection()),null==(e=this.$refs.input)||e.setSelectionRange(t.start,t.end+1)},selectFirst(){this.select(this.pattern.parts[0])},selectLast(){this.select(this.pattern.parts[this.pattern.parts.length-1])},selectNext(t){this.select(this.pattern.parts[t+1])},selectPrev(t){this.select(this.pattern.parts[t-1])},selection(){return this.pattern.at(this.$refs.input.selectionStart,this.$refs.input.selectionEnd)},toDatetime(t){return this.round(this.$library.dayjs.iso(t,this.inputType))},toISO(t){return(null==t?void 0:t.toISO(this.inputType))||null}},validations(){return{value:{min:!this.dt||!this.min||(()=>this.dt.validate(this.min,"min",this.rounding.unit)),max:!this.dt||!this.max||(()=>this.dt.validate(this.max,"max",this.rounding.unit)),required:!this.required||(()=>!!this.dt)}}}},(function(){var t=this;return(0,t._self._c)("input",{directives:[{name:"model",rawName:"v-model",value:t.formatted,expression:"formatted"},{name:"direction",rawName:"v-direction"}],ref:"input",class:`k-text-input k-${t.type}-input`,attrs:{id:t.id,autofocus:t.autofocus,disabled:t.disabled,placeholder:t.display,required:t.required,autocomplete:"off",spellcheck:"false",type:"text"},domProps:{value:t.formatted},on:{blur:t.onBlur,focus:function(e){return t.$emit("focus")},input:[function(e){e.target.composing||(t.formatted=e.target.value)},function(e){return t.onInput(e.target.value)}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.stopPropagation(),e.preventDefault(),t.onArrowDown.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.stopPropagation(),e.preventDefault(),t.onArrowUp.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.stopPropagation(),e.preventDefault(),t.onEnter.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:t.onTab.apply(null,arguments)}]}})}),[],!1,null,null,null,null).exports,as={mixins:[Zt,te,se,oe,re],props:{autocomplete:{type:[Boolean,String],default:"off"},maxlength:Number,minlength:Number,pattern:String,placeholder:String,preselect:Boolean,spellcheck:{type:[Boolean,String],default:"off"},type:{type:String,default:"text"},value:String}};const us=It({mixins:[as],inheritAttrs:!1,data(){return{listeners:{...this.$listeners,input:t=>this.onInput(t.target.value)}}},watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{focus(){this.$refs.input.focus()},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.$refs.input.select()}},validations(){return{value:{required:!this.required||P.required,minLength:!this.minlength||P.minLength(this.minlength),maxLength:!this.maxlength||P.maxLength(this.maxlength),email:"email"!==this.type||P.email,url:"url"!==this.type||P.url,pattern:!this.pattern||(t=>!this.required&&!t||!this.$refs.input.validity.patternMismatch)}}}},(function(){var t=this;return(0,t._self._c)("input",t._g(t._b({directives:[{name:"direction",rawName:"v-direction"}],ref:"input",staticClass:"k-text-input"},"input",{autocomplete:t.autocomplete,autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,pattern:t.pattern,placeholder:t.placeholder,required:t.required,spellcheck:t.spellcheck,type:t.type,value:t.value},!1),t.listeners))}),[],!1,null,null,null,null).exports,cs={mixins:[as],props:{autocomplete:{type:String,default:"email"},placeholder:{type:String,default:()=>window.panel.$t("email.placeholder")},type:{type:String,default:"email"}}};const ds=It({extends:us,mixins:[cs]},null,null,!1,null,null,null,null).exports;class ps extends Se{get schema(){return{content:"bulletList|orderedList"}}}const hs=It({inheritAttrs:!1,props:{autofocus:Boolean,marks:{type:[Array,Boolean],default:!0},value:String},data(){return{list:this.value,html:this.value}},computed:{extensions:()=>[new ps({inline:!0})]},watch:{value(t){t!==this.html&&(this.list=t,this.html=t)}},methods:{focus(){this.$refs.input.focus()},onInput(t){let e=(new DOMParser).parseFromString(t,"text/html").querySelector("ul, ol");e&&0!==e.textContent.trim().length?(this.list=t,this.html=t.replace(/(

|<\/p>)/gi,""),this.$emit("input",this.html)):this.$emit("input",this.list="")}}},(function(){var t=this;return(0,t._self._c)("k-writer",t._b({ref:"input",staticClass:"k-list-input",attrs:{extensions:t.extensions,nodes:["bulletList","orderedList"],value:t.list},on:{input:t.onInput}},"k-writer",t.$props,!1))}),[],!1,null,null,null,null).exports,ms={mixins:[te,se,re],props:{max:Number,min:Number,layout:String,options:{type:Array,default:()=>[]},search:[Object,Boolean],separator:{type:String,default:","},sort:Boolean,value:{type:Array,required:!0,default:()=>[]}}};const fs=It({mixins:[ms],inheritAttrs:!1,data(){return{state:this.value,q:null,limit:!0,scrollTop:0}},computed:{draggable(){return this.state.length>1&&!this.sort},dragOptions(){return{disabled:!this.draggable,draggable:".k-tag",delay:1}},emptyLabel(){return this.q?this.$t("search.results.none"):this.$t("options.none")},filtered(){var t;return(null==(t=this.q)?void 0:t.length)>=(this.search.min||0)?this.options.filter((t=>this.isFiltered(t))).map((t=>({...t,display:this.toHighlightedString(t.text),info:this.toHighlightedString(t.value)}))):this.options.map((t=>({...t,display:t.text,info:t.value})))},more(){return!this.max||this.state.lengththis.options.findIndex((e=>e.value===t.value));return t.sort(((t,s)=>e(t)-e(s)))},visible(){return this.limit?this.filtered.slice(0,this.search.display||this.filtered.length):this.filtered}},watch:{value(t){this.state=t,this.onInvalid()}},mounted(){this.onInvalid(),this.$events.$on("click",this.close),this.$events.$on("keydown.cmd.s",this.close)},destroyed(){this.$events.$off("click",this.close),this.$events.$off("keydown.cmd.s",this.close)},methods:{add(t){!0===this.more&&(this.state.push(t),this.onInput())},blur(){this.close()},close(){!0===this.$refs.dropdown.isOpen&&(this.$refs.dropdown.close(),this.limit=!0)},escape(){this.q?this.q=null:this.close()},focus(){this.$refs.dropdown.open()},index(t){return this.state.findIndex((e=>e.value===t.value))},isFiltered(t){return String(t.text).match(this.regex)||String(t.value).match(this.regex)},isSelected(t){return-1!==this.index(t)},navigate(t){var e,s,n;"prev"===t&&(t="previous"),null==(n=null==(s=null==(e=document.activeElement)?void 0:e[t+"Sibling"])?void 0:s.focus)||n.call(s)},onClose(){!1===this.$refs.dropdown.isOpen&&(document.activeElement===this.$parent.$el&&(this.q=null),this.$parent.$el.focus())},onInput(){this.$emit("input",this.sorted)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOpen(){this.$nextTick((()=>{var t,e;null==(e=null==(t=this.$refs.search)?void 0:t.focus)||e.call(t),this.$refs.dropdown.$el.querySelector(".k-multiselect-options").scrollTop=this.scrollTop}))},remove(t){this.state.splice(this.index(t),1),this.onInput()},select(t){this.scrollTop=this.$refs.dropdown.$el.querySelector(".k-multiselect-options").scrollTop,t={text:t.text,value:t.value},this.isSelected(t)?this.remove(t):this.add(t)},toHighlightedString(t){return(t=this.$helper.string.stripHTML(t)).replace(this.regex,"$1")}},validations(){return{state:{required:!this.required||P.required,minLength:!this.min||P.minLength(this.min),maxLength:!this.max||P.maxLength(this.max)}}}},(function(){var t=this,e=t._self._c;return e("k-draggable",{staticClass:"k-multiselect-input",attrs:{list:t.state,options:t.dragOptions,"data-layout":t.layout,element:"k-dropdown"},on:{end:t.onInput},nativeOn:{click:function(e){return t.$refs.dropdown.toggle.apply(null,arguments)}},scopedSlots:t._u([{key:"footer",fn:function(){return[e("k-dropdown-content",{ref:"dropdown",on:{open:t.onOpen,close:t.onClose},nativeOn:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:(e.stopPropagation(),t.close.apply(null,arguments))}}},[t.search?e("k-dropdown-item",{staticClass:"k-multiselect-search",attrs:{icon:"search"}},[e("input",{directives:[{name:"model",rawName:"v-model",value:t.q,expression:"q"}],ref:"search",attrs:{placeholder:t.search.min?t.$t("search.min",{min:t.search.min}):t.$t("search")+" …"},domProps:{value:t.q},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:(e.stopPropagation(),t.escape.apply(null,arguments))},input:function(e){e.target.composing||(t.q=e.target.value)}}})]):t._e(),e("div",{staticClass:"k-multiselect-options scroll-y-auto"},[t._l(t.visible,(function(s){return e("k-dropdown-item",{key:s.value,class:{"k-multiselect-option":!0,selected:t.isSelected(s),disabled:!t.more},attrs:{icon:t.isSelected(s)?"check":"circle-outline"},on:{click:function(e){return e.preventDefault(),t.select(s)}},nativeOn:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.preventDefault(),e.stopPropagation(),t.select(s))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"space",32,e.key,[" ","Spacebar"])?null:(e.preventDefault(),e.stopPropagation(),t.select(s))}]}},[e("span",{domProps:{innerHTML:t._s(s.display)}}),e("span",{staticClass:"k-multiselect-value",domProps:{innerHTML:t._s(s.info)}})])})),0===t.filtered.length?e("k-dropdown-item",{staticClass:"k-multiselect-option",attrs:{disabled:!0}},[t._v(" "+t._s(t.emptyLabel)+" ")]):t._e()],2),t.visible.lengththis.onInput(t.target.value),blur:this.onBlur}}},watch:{value(t){this.number=t},number:{immediate:!0,handler(){this.onInvalid()}}},mounted(){this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{decimals(){const t=Number(this.step||0);return Math.floor(t)===t?0:-1!==t.toString().indexOf("e")?parseInt(t.toFixed(16).split(".")[1].split("").reverse().join("")).toString().length:t.toString().split(".")[1].length||0},format(t){if(isNaN(t)||""===t)return"";const e=this.decimals();return t=e?parseFloat(t).toFixed(e):Number.isInteger(this.step)?parseInt(t):parseFloat(t)},clean(){this.number=this.format(this.number)},emit(t){t=parseFloat(t),isNaN(t)&&(t=""),t!==this.value&&this.$emit("input",t)},focus(){this.$refs.input.focus()},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput(t){this.number=t,this.emit(t)},onBlur(){this.clean(),this.emit(this.number)},select(){this.$refs.input.select()}},validations(){return{value:{required:!this.required||P.required,min:!this.min||P.minValue(this.min),max:!this.max||P.maxValue(this.max)}}}},(function(){var t=this;return(0,t._self._c)("input",t._g(t._b({ref:"input",staticClass:"k-number-input",attrs:{step:t.stepNumber,type:"number"},domProps:{value:t.number},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.ctrlKey?t.clean.apply(null,arguments):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.metaKey?t.clean.apply(null,arguments):null}]}},"input",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,max:t.max,min:t.min,name:t.name,placeholder:t.placeholder,required:t.required},!1),t.listeners))}),[],!1,null,null,null,null).exports,bs={mixins:[as],props:{autocomplete:{type:String,default:"new-password"},type:{type:String,default:"password"}}};const ys=It({extends:us,mixins:[bs]},null,null,!1,null,null,null,null).exports,vs={mixins:[Zt,te,se,re],props:{columns:Number,options:Array,value:[String,Number,Boolean]}};const $s=It({mixins:[vs],inheritAttrs:!1,watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$el.querySelector("input").focus()},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()}},validations(){return{value:{required:!this.required||P.required}}}},(function(){var t=this,e=t._self._c;return e("ul",{staticClass:"k-radio-input",style:"--columns:"+t.columns},t._l(t.options,(function(s,n){return e("li",{key:n},[e("input",{staticClass:"k-radio-input-native",attrs:{id:t.id+"-"+n,name:t.id,type:"radio"},domProps:{value:s.value,checked:t.value===s.value},on:{change:function(e){return t.onInput(s.value)}}}),s.info?e("label",{attrs:{for:t.id+"-"+n}},[e("span",{staticClass:"k-radio-input-text",domProps:{innerHTML:t._s(s.text)}}),e("span",{staticClass:"k-radio-input-info",domProps:{innerHTML:t._s(s.info)}})]):e("label",{attrs:{for:t.id+"-"+n},domProps:{innerHTML:t._s(s.text)}}),s.icon?e("k-icon",{attrs:{type:s.icon}}):t._e()],1)})),0)}),[],!1,null,null,null,null).exports,_s={mixins:[Zt,te,se,oe,re],props:{default:[Number,String],max:{type:Number,default:100},min:{type:Number,default:0},step:{type:Number,default:1},tooltip:{type:[Boolean,Object],default:()=>({before:null,after:null})},value:[Number,String]}};const xs=It({mixins:[_s],inheritAttrs:!1,data(){return{listeners:{...this.$listeners,input:t=>this.onInput(t.target.value)}}},computed:{baseline(){return this.min<0?0:this.min},label(){return this.required||this.value||0===this.value?this.format(this.position):"–"},position(){return this.value||0===this.value?this.value:this.default||this.baseline}},watch:{position(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},format(t){const e=document.lang?document.lang.replace("_","-"):"en",s=this.step.toString().split("."),n=s.length>1?s[1].length:0;return new Intl.NumberFormat(e,{minimumFractionDigits:n}).format(t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput(t){this.$emit("input",t)}},validations(){return{position:{required:!this.required||P.required,min:!this.min||P.minValue(this.min),max:!this.max||P.maxValue(this.max)}}}},(function(){var t=this,e=t._self._c;return e("label",{staticClass:"k-range-input"},[e("input",t._g(t._b({ref:"input",staticClass:"k-range-input-native",style:`--min: ${t.min}; --max: ${t.max}; --value: ${t.position}`,attrs:{type:"range"},domProps:{value:t.position}},"input",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,max:t.max,min:t.min,name:t.name,required:t.required,step:t.step},!1),t.listeners)),t.tooltip?e("span",{staticClass:"k-range-input-tooltip"},[t.tooltip.before?e("span",{staticClass:"k-range-input-tooltip-before"},[t._v(t._s(t.tooltip.before))]):t._e(),e("span",{staticClass:"k-range-input-tooltip-text"},[t._v(t._s(t.label))]),t.tooltip.after?e("span",{staticClass:"k-range-input-tooltip-after"},[t._v(t._s(t.tooltip.after))]):t._e()]):t._e()])}),[],!1,null,null,null,null).exports,ws={mixins:[Zt,te,se,oe,re],props:{ariaLabel:String,default:String,empty:{type:[Boolean,String],default:!0},placeholder:String,options:{type:Array,default:()=>[]},value:{type:[String,Number,Boolean],default:""}}};const Ss=It({mixins:[ws],inheritAttrs:!1,data(){return{selected:this.value,listeners:{...this.$listeners,click:t=>this.onClick(t),change:t=>this.onInput(t.target.value),input:()=>{}}}},computed:{emptyOption(){return this.placeholder||"—"},hasEmptyOption(){return!1!==this.empty&&!(this.required&&this.default)},label(){const t=this.text(this.selected);return""===this.selected||null===this.selected||null===t?this.emptyOption:t}},watch:{value(t){this.selected=t,this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},onClick(t){t.stopPropagation(),this.$emit("click",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onInput(t){this.selected=t,this.$emit("input",this.selected)},select(){this.focus()},text(t){let e=null;return this.options.forEach((s=>{s.value==t&&(e=s.text)})),e}},validations(){return{selected:{required:!this.required||P.required}}}},(function(){var t=this,e=t._self._c;return e("span",{staticClass:"k-select-input",attrs:{"data-disabled":t.disabled,"data-empty":""===t.selected}},[e("select",t._g({ref:"input",staticClass:"k-select-input-native",attrs:{id:t.id,autofocus:t.autofocus,"aria-label":t.ariaLabel,disabled:t.disabled,name:t.name,required:t.required},domProps:{value:t.selected}},t.listeners),[t.hasEmptyOption?e("option",{attrs:{disabled:t.required,value:""}},[t._v(" "+t._s(t.emptyOption)+" ")]):t._e(),t._l(t.options,(function(s){return e("option",{key:s.value,attrs:{disabled:s.disabled},domProps:{value:s.value}},[t._v(" "+t._s(s.text)+" ")])}))],2),t._v(" "+t._s(t.label)+" ")])}),[],!1,null,null,null,null).exports,Cs={mixins:[as],props:{allow:{type:String,default:""},formData:{type:Object,default:()=>({})},sync:{type:String}}};const Os=It({extends:us,mixins:[Cs],data(){return{slug:this.sluggify(this.value),slugs:this.$language?this.$language.rules:this.$system.slugs,syncValue:null}},watch:{formData:{handler(t){return!this.disabled&&(!(!this.sync||void 0===t[this.sync])&&(t[this.sync]!=this.syncValue&&(this.syncValue=t[this.sync],void this.onInput(this.sluggify(this.syncValue)))))},deep:!0,immediate:!0},value(t){(t=this.sluggify(t))!==this.slug&&(this.slug=t,this.$emit("input",this.slug))}},methods:{sluggify(t){return this.$helper.slug(t,[this.slugs,this.$system.ascii],this.allow)},onInput(t){this.slug=this.sluggify(t),this.$emit("input",this.slug)}}},(function(){var t=this;return(0,t._self._c)("input",t._g(t._b({directives:[{name:"direction",rawName:"v-direction"}],ref:"input",staticClass:"k-text-input",attrs:{autocomplete:"off",spellcheck:"false",type:"text"},domProps:{value:t.slug}},"input",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,pattern:t.pattern,placeholder:t.placeholder,required:t.required},!1),t.listeners))}),[],!1,null,null,null,null).exports,As={mixins:[Zt,te,se,oe,re],props:{accept:{type:String,default:"all"},icon:{type:[String,Boolean],default:"tag"},layout:String,max:Number,min:Number,options:{type:Array,default:()=>[]},separator:{type:String,default:","},value:{type:Array,default:()=>[]}}};const Ts=It({mixins:[As],inheritAttrs:!1,data(){return{tags:this.prepareTags(this.value),selected:null,newTag:null,tagOptions:this.options.map((t=>{var e;return(null==(e=this.icon)?void 0:e.length)>0&&(t.icon=this.icon),t}),this)}},computed:{dragOptions(){return{delay:1,disabled:!this.draggable,draggable:".k-tag"}},draggable(){return this.tags.length>1},skip(){return this.tags.map((t=>t.value))}},watch:{value(t){this.tags=this.prepareTags(t),this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{addString(t){if(t)if((t=t.trim()).includes(this.separator))t.split(this.separator).forEach((t=>{this.addString(t)}));else if(0!==t.length)if("options"===this.accept){const e=this.options.filter((e=>e.text===t))[0];if(!e)return;this.addTag(e)}else this.addTag({text:t,value:t})},addTag(t){this.addTagToIndex(t),this.$refs.autocomplete.close(),this.$refs.input.focus()},addTagToIndex(t){if("options"===this.accept){if(!this.options.filter((e=>e.value===t.value))[0])return}-1===this.index(t)&&(!this.max||this.tags.length=this.tags.length)return;break;case"first":e=0;break;case"last":e=this.tags.length-1;break;default:e=t}let n=this.tags[e];if(n){let t=this.$refs[n.value];if(null==t?void 0:t[0])return{ref:t[0],tag:n,index:e}}return!1},index(t){return this.tags.findIndex((e=>e.value===t.value))},onInput(){this.$emit("input",this.tags)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},leaveInput(t){0===t.target.selectionStart&&t.target.selectionStart===t.target.selectionEnd&&0!==this.tags.length&&(this.$refs.autocomplete.close(),this.navigate("last"),t.preventDefault())},navigate(t){var e=this.get(t);e?(e.ref.focus(),this.selectTag(e.tag)):"next"===t&&(this.$refs.input.focus(),this.selectTag(null))},prepareTags:t=>!1===Array.isArray(t)?[]:t.map((t=>"string"==typeof t?{text:t,value:t}:t)),remove(t){const e=this.get("prev"),s=this.get("next");this.tags.splice(this.index(t),1),this.onInput(),e?(this.selectTag(e.tag),e.ref.focus()):s?this.selectTag(s.tag):(this.selectTag(null),this.$refs.input.focus())},select(){this.focus()},selectTag(t){this.selected=t},tab(t){var e;(null==(e=this.newTag)?void 0:e.length)>0&&(t.preventDefault(),this.addString(this.newTag))},type(t){this.newTag=t,this.$refs.autocomplete.search(t)}},validations(){return{tags:{required:!this.required||P.required,minLength:!this.min||P.minLength(this.min),maxLength:!this.max||P.maxLength(this.max)}}}},(function(){var t=this,e=t._self._c;return e("k-draggable",{directives:[{name:"direction",rawName:"v-direction"}],ref:"box",staticClass:"k-tags-input",attrs:{list:t.tags,"data-layout":t.layout,options:t.dragOptions},on:{end:t.onInput},scopedSlots:t._u([{key:"footer",fn:function(){return[e("span",{staticClass:"k-tags-input-element"},[e("k-autocomplete",{ref:"autocomplete",attrs:{html:!0,options:t.options,skip:t.skip},on:{select:t.addTag,leave:function(e){return t.$refs.input.focus()}}},[e("input",{directives:[{name:"model",rawName:"v-model.trim",value:t.newTag,expression:"newTag",modifiers:{trim:!0}}],ref:"input",attrs:{id:t.id,autofocus:t.autofocus,disabled:t.disabled||t.max&&t.tags.length>=t.max,name:t.name,autocomplete:"off",type:"text"},domProps:{value:t.newTag},on:{input:[function(e){e.target.composing||(t.newTag=e.target.value.trim())},function(e){return t.type(e.target.value)}],blur:[t.blurInput,function(e){return t.$forceUpdate()}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"s",void 0,e.key,void 0)?null:e.metaKey?t.blurInput.apply(null,arguments):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])||"button"in e&&0!==e.button||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.enter.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.tab.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"backspace",void 0,e.key,void 0)||e.ctrlKey||e.shiftKey||e.altKey||e.metaKey?null:t.leaveInput.apply(null,arguments)}]}})])],1)]},proxy:!0}])},t._l(t.tags,(function(s,n){return e("k-tag",{key:n,ref:s.value,refInFor:!0,attrs:{removable:!t.disabled,name:"tag"},on:{remove:function(e){return t.remove(s)}},nativeOn:{click:function(t){t.stopPropagation()},blur:function(e){return t.selectTag(null)},focus:function(e){return t.selectTag(s)},keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"left",37,e.key,["Left","ArrowLeft"])||"button"in e&&0!==e.button?null:t.navigate("prev")},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"right",39,e.key,["Right","ArrowRight"])||"button"in e&&2!==e.button?null:t.navigate("next")}],dblclick:function(e){return t.edit(s)}}},[t._v(" "+t._s(s.text)+" ")])})),1)}),[],!1,null,null,null,null).exports,Is={mixins:[as],props:{autocomplete:{type:String,default:"tel"},type:{type:String,default:"tel"}}};const Ms=It({extends:us,mixins:[Is]},null,null,!1,null,null,null,null).exports,Es={mixins:[Zt,te,se,oe,re],props:{buttons:{type:[Boolean,Array],default:!0},endpoints:Object,font:String,maxlength:Number,minlength:Number,placeholder:String,preselect:Boolean,size:String,spellcheck:{type:[Boolean,String],default:"off"},theme:String,uploads:[Boolean,Object,Array],value:String}};const Ls=It({mixins:[Es],inheritAttrs:!1,data:()=>({over:!1}),watch:{value(){this.onInvalid(),this.$nextTick((()=>{this.resize()}))}},mounted(){this.$nextTick((()=>{this.$library.autosize(this.$refs.input)})),this.onInvalid(),this.$props.autofocus&&this.focus(),this.$props.preselect&&this.select()},methods:{cancel(){this.$refs.input.focus()},dialog(t){if(!this.$refs[t+"Dialog"])throw"Invalid toolbar dialog";this.$refs[t+"Dialog"].open(this.$refs.input,this.selection())},focus(){this.$refs.input.focus()},insert(t){const e=this.$refs.input,s=e.value;setTimeout((()=>{if(e.focus(),document.execCommand("insertText",!1,t),e.value===s){const s=e.value.slice(0,e.selectionStart)+t+e.value.slice(e.selectionEnd);e.value=s,this.$emit("input",s)}})),this.resize()},insertFile(t){(null==t?void 0:t.length)>0&&this.insert(t.map((t=>t.dragText)).join("\n\n"))},insertUpload(t,e){this.insert(e.map((t=>t.dragText)).join("\n\n")),this.$events.$emit("model.update")},onClick(){this.$refs.toolbar&&this.$refs.toolbar.close()},onCommand(t,e){"function"==typeof this[t]?"function"==typeof e?this[t](e(this.$refs.input,this.selection())):this[t](e):window.console.warn(t+" is not a valid command")},onDrop(t){if(this.uploads&&this.$helper.isUploadEvent(t))return this.$refs.fileUpload.drop(t.dataTransfer.files,{url:this.$urls.api+"/"+this.endpoints.field+"/upload",multiple:!1});const e=this.$store.state.drag;"text"===(null==e?void 0:e.type)&&(this.focus(),this.insert(e.data))},onFocus(t){this.$emit("focus",t)},onInput(t){this.$emit("input",t.target.value)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},onOut(){this.$refs.input.blur(),this.over=!1},onOver(t){if(this.uploads&&this.$helper.isUploadEvent(t))return t.dataTransfer.dropEffect="copy",this.focus(),void(this.over=!0);const e=this.$store.state.drag;"text"===(null==e?void 0:e.type)&&(t.dataTransfer.dropEffect="copy",this.focus(),this.over=!0)},onShortcut(t){!1!==this.buttons&&"Meta"!==t.key&&"Control"!==t.key&&this.$refs.toolbar&&this.$refs.toolbar.shortcut(t.key,t)},onSubmit(t){return this.$emit("submit",t)},prepend(t){this.insert(t+" "+this.selection())},resize(){this.$library.autosize.update(this.$refs.input)},select(){this.$refs.select()},selectFile(){this.$refs.fileDialog.open({endpoint:this.endpoints.field+"/files",multiple:!1})},selection(){const t=this.$refs.input,e=t.selectionStart,s=t.selectionEnd;return t.value.substring(e,s)},uploadFile(){this.$refs.fileUpload.open({url:this.$urls.api+"/"+this.endpoints.field+"/upload",multiple:!1})},wrap(t){this.insert(t+this.selection()+t)}},validations(){return{value:{required:!this.required||P.required,minLength:!this.minlength||P.minLength(this.minlength),maxLength:!this.maxlength||P.maxLength(this.maxlength)}}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-textarea-input",attrs:{"data-over":t.over,"data-size":t.size,"data-theme":t.theme}},[e("div",{staticClass:"k-textarea-input-wrapper"},[t.buttons&&!t.disabled?e("k-toolbar",{ref:"toolbar",attrs:{buttons:t.buttons,disabled:t.disabled,uploads:t.uploads},on:{command:t.onCommand},nativeOn:{mousedown:function(t){t.preventDefault()}}}):t._e(),e("textarea",t._b({directives:[{name:"direction",rawName:"v-direction"}],ref:"input",staticClass:"k-textarea-input-native",attrs:{"data-font":t.font},on:{click:t.onClick,focus:t.onFocus,input:t.onInput,keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.metaKey?t.onSubmit.apply(null,arguments):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:e.ctrlKey?t.onSubmit.apply(null,arguments):null},function(e){return e.metaKey?t.onShortcut.apply(null,arguments):null},function(e){return e.ctrlKey?t.onShortcut.apply(null,arguments):null}],dragover:t.onOver,dragleave:t.onOut,drop:t.onDrop}},"textarea",{autofocus:t.autofocus,disabled:t.disabled,id:t.id,minlength:t.minlength,name:t.name,placeholder:t.placeholder,required:t.required,spellcheck:t.spellcheck,value:t.value},!1))],1),e("k-toolbar-email-dialog",{ref:"emailDialog",on:{cancel:t.cancel,submit:function(e){return t.insert(e)}}}),e("k-toolbar-link-dialog",{ref:"linkDialog",on:{cancel:t.cancel,submit:function(e){return t.insert(e)}}}),e("k-files-dialog",{ref:"fileDialog",on:{cancel:t.cancel,submit:function(e){return t.insertFile(e)}}}),t.uploads?e("k-upload",{ref:"fileUpload",on:{success:t.insertUpload}}):t._e()],1)}),[],!1,null,null,null,null).exports,js={props:{display:{type:String,default:"HH:mm"},max:String,min:String,step:{type:Object,default:()=>({size:5,unit:"minute"})},type:{type:String,default:"time"},value:String}};const Ds=It({mixins:[ls,js],computed:{inputType:()=>"time"}},null,null,!1,null,null,null,null).exports,Bs={props:{autofocus:Boolean,disabled:Boolean,id:[Number,String],text:{type:[Array,String]},required:Boolean,value:Boolean}};const Ps=It({mixins:[Bs],inheritAttrs:!1,computed:{label(){const t=this.text||[this.$t("off"),this.$t("on")];return Array.isArray(t)?this.value?t[1]:t[0]:t}},watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){this.$refs.input.focus()},onEnter(t){"Enter"===t.key&&this.$refs.input.click()},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.$refs.input.focus()}},validations(){return{value:{required:!this.required||P.required}}}},(function(){var t=this,e=t._self._c;return e("label",{staticClass:"k-toggle-input",attrs:{"data-disabled":t.disabled}},[e("input",{ref:"input",staticClass:"k-toggle-input-native",attrs:{id:t.id,disabled:t.disabled,type:"checkbox"},domProps:{checked:t.value},on:{change:function(e){return t.onInput(e.target.checked)}}}),e("span",{staticClass:"k-toggle-input-label",domProps:{innerHTML:t._s(t.label)}})])}),[],!1,null,null,null,null).exports,Ns={mixins:[Zt,te,se,re],props:{columns:Number,grow:Boolean,labels:Boolean,options:Array,reset:Boolean,value:[String,Number,Boolean]}};const qs=It({mixins:[Ns],inheritAttrs:!1,watch:{value(){this.onInvalid()}},mounted(){this.onInvalid(),this.$props.autofocus&&this.focus()},methods:{focus(){(this.$el.querySelector("input[checked]")||this.$el.querySelector("input")).focus()},onClick(t){t===this.value&&this.reset&&!this.required&&this.$emit("input","")},onInput(t){this.$emit("input",t)},onInvalid(){this.$emit("invalid",this.$v.$invalid,this.$v)},select(){this.focus()}},validations(){return{value:{required:!this.required||P.required}}}},(function(){var t=this,e=t._self._c;return e("ul",{staticClass:"k-toggles-input",style:"--options:"+(t.columns||t.options.length),attrs:{"data-invalid":t.$v.$invalid,"data-labels":t.labels}},t._l(t.options,(function(s,n){return e("li",{key:n},[e("input",{staticClass:"input-hidden",attrs:{id:t.id+"-"+n,name:t.id,type:"radio"},domProps:{value:s.value,checked:t.value===s.value},on:{click:function(e){return t.onClick(s.value)},change:function(e){return t.onInput(s.value)}}}),e("label",{attrs:{for:t.id+"-"+n,title:s.text}},[s.icon?e("k-icon",{attrs:{type:s.icon}}):t._e(),t.labels?e("span",{staticClass:"k-toggles-text"},[t._v(" "+t._s(s.text)+" ")]):t._e()],1)])})),0)}),[],!1,null,null,null,null).exports,Fs={mixins:[as],props:{autocomplete:{type:String,default:"url"},type:{type:String,default:"url"}}};const Rs=It({extends:us,mixins:[Fs]},null,null,!1,null,null,null,null).exports;t.component("k-checkbox-input",ns),t.component("k-checkboxes-input",os),t.component("k-date-input",ls),t.component("k-email-input",ds),t.component("k-list-input",hs),t.component("k-multiselect-input",fs),t.component("k-number-input",ks),t.component("k-password-input",ys),t.component("k-radio-input",$s),t.component("k-range-input",xs),t.component("k-select-input",Ss),t.component("k-slug-input",Os),t.component("k-tags-input",Ts),t.component("k-tel-input",Ms),t.component("k-text-input",us),t.component("k-textarea-input",Ls),t.component("k-time-input",Ds),t.component("k-toggle-input",Ps),t.component("k-toggles-input",qs),t.component("k-url-input",Rs);const zs=It({mixins:[le],inheritAttrs:!1,props:{autofocus:Boolean,empty:String,fieldsets:Object,fieldsetGroups:Object,group:String,max:{type:Number,default:null},value:{type:Array,default:()=>[]}},data:()=>({opened:[]}),computed:{hasFieldsets(){return Object.keys(this.fieldsets).length},isEmpty(){return 0===this.value.length},isFull(){return null!==this.max&&this.value.length>=this.max}},methods:{focus(){this.$refs.blocks.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-blocks-field",scopedSlots:t._u([{key:"options",fn:function(){return[t.hasFieldsets?e("k-dropdown",[e("k-button",{attrs:{icon:"dots"},on:{click:function(e){return t.$refs.options.toggle()}}}),e("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[e("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"add"},on:{click:function(e){return t.$refs.blocks.choose(t.value.length)}}},[t._v(" "+t._s(t.$t("add"))+" ")]),e("hr"),e("k-dropdown-item",{attrs:{disabled:t.isEmpty,icon:"template"},on:{click:function(e){return t.$refs.blocks.copyAll()}}},[t._v(" "+t._s(t.$t("copy.all"))+" ")]),e("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"download"},on:{click:function(e){return t.$refs.blocks.pasteboard()}}},[t._v(" "+t._s(t.$t("paste"))+" ")]),e("hr"),e("k-dropdown-item",{attrs:{disabled:t.isEmpty,icon:"trash"},on:{click:function(e){return t.$refs.blocks.confirmToRemoveAll()}}},[t._v(" "+t._s(t.$t("delete.all"))+" ")])],1)],1):t._e()]},proxy:!0}])},"k-field",t.$props,!1),[e("k-blocks",t._g({ref:"blocks",attrs:{autofocus:t.autofocus,compact:!1,empty:t.empty,endpoints:t.endpoints,fieldsets:t.fieldsets,"fieldset-groups":t.fieldsetGroups,group:t.group,max:t.max,value:t.value},on:{close:function(e){t.opened=e},open:function(e){t.opened=e}}},t.$listeners))],1)}),[],!1,null,null,null,null).exports,Ys={props:{counter:{type:Boolean,default:!0}},computed:{counterOptions(){var t,e;if(null===this.value||this.disabled||!1===this.counter)return!1;let s=0;return this.value&&(s=Array.isArray(this.value)?this.value.length:String(this.value).length),{count:s,min:null!=(t=this.min)?t:this.minlength,max:null!=(e=this.max)?e:this.maxlength}}}};const Hs=It({mixins:[le,ce,is,Ys],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-checkboxes-field",attrs:{counter:t.counterOptions}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"checkboxes"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const Us=It({mixins:[le,ce,rs],inheritAttrs:!1,props:{calendar:{type:Boolean,default:!0},icon:{type:String,default:"calendar"},time:{type:[Boolean,Object],default:()=>({})},times:{type:Boolean,default:!0}},data(){return{isInvalid:!1,iso:this.toIso(this.value)}},computed:{isEmpty(){return this.time?null===this.iso.date&&this.iso.time:null===this.iso.date}},watch:{value(t,e){t!==e&&(this.iso=this.toIso(t))}},methods:{focus(){this.$refs.dateInput.focus()},now(){const t=this.$library.dayjs();return{date:t.toISO("date"),time:this.time?t.toISO("time"):"00:00:00"}},onInput(){if(this.isEmpty)return this.$emit("input","");const t=this.$library.dayjs.iso(this.iso.date+" "+this.iso.time);(t||null!==this.iso.date&&null!==this.iso.time)&&this.$emit("input",(null==t?void 0:t.toISO())||"")},onCalendarInput(t){var e;null==(e=this.$refs.calendar)||e.close(),this.onDateInput(t)},onDateInput(t){t&&!this.iso.time&&(this.iso.time=this.now().time),this.iso.date=t,this.onInput()},onDateInvalid(t){this.isInvalid=t},onTimeInput(t){t&&!this.iso.date&&(this.iso.date=this.now().date),this.iso.time=t,this.onInput()},onTimesInput(t){var e;null==(e=this.$refs.times)||e.close(),this.onTimeInput(t+":00")},toIso(t){const e=this.$library.dayjs.iso(t);return{date:(null==e?void 0:e.toISO("date"))||null,time:(null==e?void 0:e.toISO("time"))||null}}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-date-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("div",{ref:"body",staticClass:"k-date-field-body",attrs:{"data-invalid":!t.novalidate&&t.isInvalid,"data-theme":"field"}},[e("k-input",t._b({ref:"dateInput",attrs:{id:t._uid,autofocus:t.autofocus,disabled:t.disabled,display:t.display,max:t.max,min:t.min,required:t.required,value:t.value,theme:"field",type:"date"},on:{invalid:t.onDateInvalid,input:t.onDateInput,submit:function(e){return t.$emit("submit")}},scopedSlots:t._u([t.calendar?{key:"icon",fn:function(){return[e("k-dropdown",[e("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,tooltip:t.$t("date.select")},on:{click:function(e){return t.$refs.calendar.toggle()}}}),e("k-dropdown-content",{ref:"calendar",attrs:{align:"right"}},[e("k-calendar",{attrs:{value:t.value,min:t.min,max:t.max},on:{input:t.onCalendarInput}})],1)],1)]},proxy:!0}:null],null,!0)},"k-input",t.$props,!1)),t.time?e("k-input",{ref:"timeInput",attrs:{disabled:t.disabled,display:t.time.display,required:t.required,step:t.time.step,value:t.iso.time,icon:t.time.icon,theme:"field",type:"time"},on:{input:t.onTimeInput,submit:function(e){return t.$emit("submit")}},scopedSlots:t._u([t.times?{key:"icon",fn:function(){return[e("k-dropdown",[e("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.time.icon||"clock",tooltip:t.$t("time.select")},on:{click:function(e){return t.$refs.times.toggle()}}}),e("k-dropdown-content",{ref:"times",attrs:{align:"right"}},[e("k-times",{attrs:{display:t.time.display,value:t.value},on:{input:t.onTimesInput}})],1)],1)]},proxy:!0}:null],null,!0)}):t._e()],1)])}),[],!1,null,null,null,null).exports;const Ks=It({mixins:[le,ce,cs],inheritAttrs:!1,props:{link:{type:Boolean,default:!0},icon:{type:String,default:"email"}},computed:{mailto(){var t;return(null==(t=this.value)?void 0:t.length)>0?"mailto:"+this.value:null}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-email-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"email"},scopedSlots:t._u([{key:"icon",fn:function(){return[t.link?e("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,link:t.mailto,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"}}):t._e()]},proxy:!0}])},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports,Gs={mixins:[le],inheritAttrs:!1,props:{empty:String,info:String,link:Boolean,layout:{type:String,default:"list"},max:Number,multiple:Boolean,parent:String,search:Boolean,size:String,text:String,value:{type:Array,default:()=>[]}},data(){return{selected:this.value}},computed:{btnIcon(){return!this.multiple&&this.selected.length>0?"refresh":"add"},btnLabel(){return!this.multiple&&this.selected.length>0?this.$t("change"):this.$t("add")},collection(){return{empty:this.emptyProps,items:this.selected,layout:this.layout,link:this.link,size:this.size,sortable:!this.disabled&&this.selected.length>1}},isInvalid(){return!(!this.required||0!==this.selected.length)||(!!(this.min&&this.selected.lengththis.max))},items(){return this.models.map(this.item)},more(){return!this.max||this.max>this.selected.length}},watch:{value(t){this.selected=t}},methods:{focus(){},item:t=>t,onInput(){this.$emit("input",this.selected)},open(){if(this.disabled)return!1;this.$refs.selector.open({endpoint:this.endpoints.field,max:this.max,multiple:this.multiple,search:this.search,selected:this.selected.map((t=>t.id))})},remove(t){this.selected.splice(t,1),this.onInput()},removeById(t){this.selected=this.selected.filter((e=>e.id!==t)),this.onInput()},select(t){0!==t.length?(this.selected=this.selected.filter((e=>t.filter((t=>t.id===e.id)).length>0)),t.forEach((t=>{0===this.selected.filter((e=>t.id===e.id)).length&&this.selected.push(t)})),this.onInput()):this.selected=[]}}};const Js=It({mixins:[Gs],props:{uploads:[Boolean,Object,Array]},computed:{emptyProps(){return{icon:"image",text:this.empty||this.$t("field.files.empty")}},moreUpload(){return!this.disabled&&this.more&&this.uploads},options(){return this.uploads?{icon:this.btnIcon,text:this.btnLabel,options:[{icon:"check",text:this.$t("select"),click:"open"},{icon:"upload",text:this.$t("upload"),click:"upload"}]}:{options:[{icon:"check",text:this.$t("select"),click:()=>this.open()}]}},uploadParams(){return{accept:this.uploads.accept,max:this.max,multiple:this.multiple,url:this.$urls.api+"/"+this.endpoints.field+"/upload"}}},created(){this.$events.$on("file.delete",this.removeById)},destroyed(){this.$events.$off("file.delete",this.removeById)},methods:{drop(t){return!1!==this.uploads&&this.$refs.fileUpload.drop(t,this.uploadParams)},prompt(){if(this.disabled)return!1;this.moreUpload?this.$refs.options.toggle():this.open()},onAction(t){if(this.moreUpload)switch(t){case"open":return this.open();case"upload":return this.$refs.fileUpload.open(this.uploadParams)}},isSelected(t){return this.selected.find((e=>e.id===t.id))},upload(t,e){!1===this.multiple&&(this.selected=[]),e.forEach((t=>{this.isSelected(t)||this.selected.push(t)})),this.onInput(),this.$events.$emit("model.update")}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-files-field",scopedSlots:t._u([t.more&&!t.disabled?{key:"options",fn:function(){return[e("k-button-group",{staticClass:"k-field-options"},[e("k-options-dropdown",t._b({ref:"options",on:{action:t.onAction}},"k-options-dropdown",t.options,!1))],1)]},proxy:!0}:null],null,!0)},"k-field",t.$props,!1),[e("k-dropzone",{attrs:{disabled:!t.moreUpload},on:{drop:t.drop}},[e("k-collection",t._b({on:{empty:t.prompt,sort:t.onInput,sortChange:function(e){return t.$emit("change",e)}},scopedSlots:t._u([{key:"options",fn:function({index:s}){return[t.disabled?t._e():e("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(s)}}})]}}])},"k-collection",t.collection,!1))],1),e("k-files-dialog",{ref:"selector",on:{submit:t.select}}),e("k-upload",{ref:"fileUpload",on:{success:t.upload}})],1)}),[],!1,null,null,null,null).exports;const Vs=It({},(function(){return(0,this._self._c)("div",{staticClass:"k-field k-gap-field"})}),[],!1,null,null,null,null).exports;const Ws=It({mixins:[ee,ie],props:{numbered:Boolean}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-headline-field"},[e("k-headline",{attrs:{"data-numbered":t.numbered,size:"large"}},[t._v(" "+t._s(t.label)+" ")]),t.help?e("footer",{staticClass:"k-field-footer"},[t.help?e("k-text",{staticClass:"k-field-help",attrs:{theme:"help",html:t.help}}):t._e()],1):t._e()],1)}),[],!1,null,null,null,null).exports;const Xs=It({mixins:[ee,ie],props:{text:String,theme:{type:String,default:"info"}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-field k-info-field"},[e("k-headline",[t._v(t._s(t.label))]),e("k-box",{attrs:{theme:t.theme}},[e("k-text",{attrs:{html:t.text}})],1),t.help?e("footer",{staticClass:"k-field-footer"},[t.help?e("k-text",{staticClass:"k-field-help",attrs:{theme:"help",html:t.help}}):t._e()],1):t._e()],1)}),[],!1,null,null,null,null).exports;const Zs=It({components:{"k-block-layouts":It({components:{"k-layout":It({components:{"k-layout-column":It({props:{blocks:Array,endpoints:Object,fieldsetGroups:Object,fieldsets:Object,id:String,isSelected:Boolean,width:String}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-column k-layout-column",attrs:{id:t.id,"data-width":t.width,tabindex:"0"},on:{dblclick:function(e){return t.$refs.blocks.choose(t.blocks.length)}}},[e("k-blocks",{ref:"blocks",attrs:{endpoints:t.endpoints,"fieldset-groups":t.fieldsetGroups,fieldsets:t.fieldsets,value:t.blocks,group:"layout"},on:{input:function(e){return t.$emit("input",e)}},nativeOn:{dblclick:function(t){t.stopPropagation()}}})],1)}),[],!1,null,null,null,null).exports},props:{attrs:[Array,Object],columns:Array,disabled:Boolean,endpoints:Object,fieldsetGroups:Object,fieldsets:Object,id:String,isSelected:Boolean,settings:Object},computed:{tabs(){let t=this.settings.tabs;return Object.entries(t).forEach((([e,s])=>{Object.entries(s.fields).forEach((([s])=>{t[e].fields[s].endpoints={field:this.endpoints.field+"/fields/"+s,section:this.endpoints.section,model:this.endpoints.model}}))})),t}}},(function(){var t=this,e=t._self._c;return e("section",{staticClass:"k-layout",attrs:{"data-selected":t.isSelected,tabindex:"0"},on:{click:function(e){return t.$emit("select")}}},[e("k-grid",{staticClass:"k-layout-columns"},t._l(t.columns,(function(s,n){return e("k-layout-column",t._b({key:s.id,attrs:{endpoints:t.endpoints,"fieldset-groups":t.fieldsetGroups,fieldsets:t.fieldsets},on:{input:function(e){return t.$emit("updateColumn",{column:s,columnIndex:n,blocks:e})}}},"k-layout-column",s,!1))})),1),t.disabled?t._e():e("nav",{staticClass:"k-layout-toolbar"},[t.settings?e("k-button",{staticClass:"k-layout-toolbar-button",attrs:{tooltip:t.$t("settings"),icon:"settings"},on:{click:function(e){return t.$refs.drawer.open()}}}):t._e(),e("k-dropdown",[e("k-button",{staticClass:"k-layout-toolbar-button",attrs:{icon:"angle-down"},on:{click:function(e){return t.$refs.options.toggle()}}}),e("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[e("k-dropdown-item",{attrs:{icon:"angle-up"},on:{click:function(e){return t.$emit("prepend")}}},[t._v(" "+t._s(t.$t("insert.before"))+" ")]),e("k-dropdown-item",{attrs:{icon:"angle-down"},on:{click:function(e){return t.$emit("append")}}},[t._v(" "+t._s(t.$t("insert.after"))+" ")]),e("hr"),t.settings?e("k-dropdown-item",{attrs:{icon:"settings"},on:{click:function(e){return t.$refs.drawer.open()}}},[t._v(" "+t._s(t.$t("settings"))+" ")]):t._e(),e("k-dropdown-item",{attrs:{icon:"copy"},on:{click:function(e){return t.$emit("duplicate")}}},[t._v(" "+t._s(t.$t("duplicate"))+" ")]),e("hr"),e("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){return t.$refs.confirmRemoveDialog.open()}}},[t._v(" "+t._s(t.$t("field.layout.delete"))+" ")])],1)],1),e("k-sort-handle")],1),t.settings?e("k-form-drawer",{ref:"drawer",staticClass:"k-layout-drawer",attrs:{tabs:t.tabs,title:t.$t("settings"),value:t.attrs,icon:"settings"},on:{input:function(e){return t.$emit("updateAttrs",e)}}}):t._e(),e("k-remove-dialog",{ref:"confirmRemoveDialog",attrs:{text:t.$t("field.layout.delete.confirm")},on:{submit:function(e){return t.$emit("remove")}}})],1)}),[],!1,null,null,null,null).exports},props:{disabled:Boolean,empty:String,endpoints:Object,fieldsetGroups:Object,fieldsets:Object,layouts:Array,max:Number,settings:Object,value:Array},data(){return{currentLayout:null,nextIndex:null,rows:this.value,selected:null}},computed:{draggableOptions(){return{id:this._uid,handle:!0,list:this.rows}}},watch:{value(){this.rows=this.value}},methods:{async addLayout(t){let e=await this.$api.post(this.endpoints.field+"/layout",{columns:t});this.rows.splice(this.nextIndex,0,e),this.layouts.length>1&&this.$refs.selector.close(),this.save()},duplicateLayout(t,e){let s={...this.$helper.clone(e),id:this.$helper.uuid()};s.columns=s.columns.map((t=>(t.id=this.$helper.uuid(),t.blocks=t.blocks.map((t=>(t.id=this.$helper.uuid(),t))),t))),this.rows.splice(t+1,0,s),this.save()},removeLayout(t){const e=this.rows.findIndex((e=>e.id===t.id));-1!==e&&this.$delete(this.rows,e),this.save()},save(){this.$emit("input",this.rows)},selectLayout(t){this.nextIndex=t,1!==this.layouts.length?this.$refs.selector.open():this.addLayout(this.layouts[0])},updateColumn(t){this.rows[t.layoutIndex].columns[t.columnIndex].blocks=t.blocks,this.save()},updateAttrs(t,e){this.rows[t].attrs=e,this.save()}}},(function(){var t=this,e=t._self._c;return e("div",[t.rows.length?[e("k-draggable",t._b({staticClass:"k-layouts",on:{sort:t.save}},"k-draggable",t.draggableOptions,!1),t._l(t.rows,(function(s,n){return e("k-layout",t._b({key:s.id,attrs:{disabled:t.disabled,endpoints:t.endpoints,"fieldset-groups":t.fieldsetGroups,fieldsets:t.fieldsets,"is-selected":t.selected===s.id,settings:t.settings},on:{append:function(e){return t.selectLayout(n+1)},duplicate:function(e){return t.duplicateLayout(n,s)},prepend:function(e){return t.selectLayout(n)},remove:function(e){return t.removeLayout(s)},select:function(e){t.selected=s.id},updateAttrs:function(e){return t.updateAttrs(n,e)},updateColumn:function(e){return t.updateColumn({layout:s,layoutIndex:n,...e})}}},"k-layout",s,!1))})),1),t.disabled?t._e():e("k-button",{staticClass:"k-layout-add-button",attrs:{icon:"add"},on:{click:function(e){return t.selectLayout(t.rows.length)}}})]:[e("k-empty",{staticClass:"k-layout-empty",attrs:{icon:"dashboard"},on:{click:function(e){return t.selectLayout(0)}}},[t._v(" "+t._s(t.empty||t.$t("field.layout.empty"))+" ")])],e("k-dialog",{ref:"selector",staticClass:"k-layout-selector",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"}},[e("k-headline",[t._v(t._s(t.$t("field.layout.select")))]),e("ul",t._l(t.layouts,(function(s,n){return e("li",{key:n,staticClass:"k-layout-selector-option"},[e("k-grid",{nativeOn:{click:function(e){return t.addLayout(s)}}},t._l(s,(function(t,s){return e("k-column",{key:s,attrs:{width:t}})})),1)],1)})),0)],1)],2)}),[],!1,null,null,null,null).exports},mixins:[le],inheritAttrs:!1,props:{empty:String,fieldsetGroups:Object,fieldsets:Object,layouts:{type:Array,default:()=>[["1/1"]]},settings:Object,value:{type:Array,default:()=>[]}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-layout-field"},"k-field",t.$props,!1),[e("k-block-layouts",t._b({on:{input:function(e){return t.$emit("input",e)}}},"k-block-layouts",t.$props,!1))],1)}),[],!1,null,null,null,null).exports;const Qs=It({},(function(){return(0,this._self._c)("hr",{staticClass:"k-line-field"})}),[],!1,null,null,null,null).exports;const tn=It({mixins:[le,ce],inheritAttrs:!1,props:{marks:[Array,Boolean],value:String},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-list-field",attrs:{input:t._uid,counter:!1}},"k-field",t.$props,!1),[e("k-input",t._b({ref:"input",attrs:{id:t._uid,marks:t.marks,value:t.value,type:"list",theme:"field"},on:{input:function(e){return t.$emit("input",e)}}},"k-input",t.$props,!1))],1)}),[],!1,null,null,null,null).exports;const en=It({mixins:[le,ce,ms,Ys],inheritAttrs:!1,props:{icon:{type:String,default:"angle-down"}},mounted(){this.$refs.input.$el.setAttribute("tabindex",0)},methods:{blur(t){this.$refs.input.blur(t)},focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-multiselect-field",attrs:{input:t._uid,counter:t.counterOptions},on:{blur:t.blur},nativeOn:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:(e.preventDefault(),t.focus.apply(null,arguments))}}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"multiselect"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const sn=It({mixins:[le,ce,gs],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-number-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"number"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const nn=It({mixins:[Gs],computed:{emptyProps(){return{icon:"page",text:this.empty||this.$t("field.pages.empty")}}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-pages-field",scopedSlots:t._u([{key:"options",fn:function(){return[e("k-button-group",{staticClass:"k-field-options"},[t.more&&!t.disabled?e("k-button",{staticClass:"k-field-options-button",attrs:{icon:t.btnIcon,text:t.btnLabel},on:{click:t.open}}):t._e()],1)]},proxy:!0}])},"k-field",t.$props,!1),[e("k-collection",t._b({on:{empty:t.open,sort:t.onInput,sortChange:function(e){return t.$emit("change",e)}},scopedSlots:t._u([{key:"options",fn:function({index:s}){return[t.disabled?t._e():e("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(s)}}})]}}])},"k-collection",t.collection,!1)),e("k-pages-dialog",{ref:"selector",on:{submit:t.select}})],1)}),[],!1,null,null,null,null).exports;const on=It({mixins:[le,ce,bs,Ys],inheritAttrs:!1,props:{minlength:{type:Number,default:8},icon:{type:String,default:"key"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-password-field",attrs:{input:t._uid,counter:t.counterOptions},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options")]},proxy:!0}],null,!0)},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"password"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const rn=It({mixins:[le,ce,vs],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-radio-field"},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"radio"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const ln=It({mixins:[ce,le,_s],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-range-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"range"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const an=It({mixins:[le,ce,ws],inheritAttrs:!1,props:{icon:{type:String,default:"angle-down"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-select-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"select"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const un=It({mixins:[le,ce,Cs],inheritAttrs:!1,props:{icon:{type:String,default:"url"},path:{type:String},wizard:{type:[Boolean,Object],default:!1}},data(){return{slug:this.value}},computed:{preview(){return void 0!==this.help?this.help:void 0!==this.path?this.path+this.value:null}},watch:{value(){this.slug=this.value}},methods:{focus(){this.$refs.input.focus()},onWizard(){var t;this.formData[null==(t=this.wizard)?void 0:t.field]&&(this.slug=this.formData[this.wizard.field])}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-slug-field",attrs:{input:t._uid,help:t.preview},scopedSlots:t._u([t.wizard&&t.wizard.text?{key:"options",fn:function(){return[e("k-button",{attrs:{text:t.wizard.text,icon:"wand"},on:{click:t.onWizard}})]},proxy:!0}:null],null,!0)},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,value:t.slug,theme:"field",type:"slug"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const cn=It({mixins:[le],inheritAttrs:!1,props:{columns:Object,duplicate:{type:Boolean,default:!0},empty:String,fields:Object,limit:Number,max:Number,min:Number,prepend:{type:Boolean,default:!1},sortable:{type:Boolean,default:!0},sortBy:String,value:{type:Array,default:()=>[]}},data(){return{autofocus:null,items:this.toItems(this.value),currentIndex:null,currentModel:null,trash:null,page:1}},computed:{dragOptions(){return{disabled:!this.isSortable,fallbackClass:"k-sortable-row-fallback"}},form(){let t={};return Object.keys(this.fields).forEach((e=>{let s=this.fields[e];s.section=this.name,s.endpoints={field:this.endpoints.field+"+"+e,section:this.endpoints.section,model:this.endpoints.model},null===this.autofocus&&!0===s.autofocus&&(this.autofocus=e),t[e]=s})),t},index(){return this.limit?(this.page-1)*this.limit:1},more(){return!0!==this.disabled&&!(this.max&&this.items.length>=this.max)},isInvalid(){return!0!==this.disabled&&(!!(this.min&&this.items.lengththis.max))},isSortable(){return!this.sortBy&&(!this.limit&&(!0!==this.disabled&&(!(this.items.length<=1)&&!1!==this.sortable)))},pagination(){let t=0;return this.limit&&(t=(this.page-1)*this.limit),{page:this.page,offset:t,limit:this.limit,total:this.items.length,align:"center",details:!0}},options(){let t=[],e=this.duplicate&&this.more&&null===this.currentIndex;return e&&t.push({icon:"copy",text:this.$t("duplicate"),click:"duplicate"}),t.push({icon:"remove",text:e?this.$t("remove"):null,click:"remove"}),t},paginatedItems(){return this.limit?this.items.slice(this.pagination.offset,this.pagination.offset+this.limit):this.items}},watch:{value(t){t!=this.items&&(this.items=this.toItems(t))}},methods:{add(t){!0===this.prepend?this.items.unshift(t):this.items.push(t)},focus(){var t,e;null==(e=null==(t=this.$refs.add)?void 0:t.focus)||e.call(t)},jump(t,e){this.open(t+this.pagination.offset,e)},onAdd(){if(!0===this.disabled)return!1;if(null!==this.currentIndex)return this.onFormDiscard(),!1;let t={};for(const e in this.fields)t[e]=this.$helper.clone(this.fields[e].default);this.currentIndex="new",this.currentModel=t,this.onFormOpen()},onFormClose(){this.currentIndex=null,this.currentModel=null},onFormDiscard(){if("new"===this.currentIndex){if(0===Object.values(this.currentModel).filter((t=>!1===this.$helper.object.isEmpty(t))).length)return void this.onFormClose()}this.onFormSubmit()},onFormOpen(t=this.autofocus){this.$nextTick((()=>{var e;null==(e=this.$refs.form)||e.focus(t)}))},async onFormPaginate(t){await this.save(),this.open(t)},async onFormSubmit(){try{await this.save(),this.onFormClose()}catch(t){}},onInput(t=this.items){this.$emit("input",t)},onOption(t,e,s){switch(t){case"remove":this.onFormClose(),this.trash=s+this.pagination.offset,this.$refs.remove.open();break;case"duplicate":this.add(this.items[s+this.pagination.offset]),this.onInput()}},onRemove(){if(null===this.trash)return!1;this.items.splice(this.trash,1),this.trash=null,this.$refs.remove.close(),this.onInput(),0===this.paginatedItems.length&&this.page>1&&this.page--,this.items=this.sort(this.items)},open(t,e){this.currentIndex=t,this.currentModel=this.$helper.clone(this.items[t]),this.onFormOpen(e)},paginate({page:t}){this.page=t},sort(t){return this.sortBy?t.sortBy(this.sortBy):t},async save(){if(null!==this.currentIndex&&void 0!==this.currentIndex)try{return await this.validate(this.currentModel),"new"===this.currentIndex?this.add(this.currentModel):this.items[this.currentIndex]=this.currentModel,this.items=this.sort(this.items),this.onInput(),!0}catch(t){throw this.$store.dispatch("notification/error",{message:this.$t("error.form.incomplete"),details:t}),t}},toItems(t){return!1===Array.isArray(t)?[]:this.sort(t)},async validate(t){const e=await this.$api.post(this.endpoints.field+"/validate",t);if(e.length>0)throw e;return!0}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-structure-field",nativeOn:{click:function(t){t.stopPropagation()}},scopedSlots:t._u([{key:"options",fn:function(){return[t.more&&null===t.currentIndex?e("k-button",{ref:"add",attrs:{id:t._uid,text:t.$t("add"),icon:"add"},on:{click:t.onAdd}}):t._e()]},proxy:!0}])},"k-field",t.$props,!1),[null!==t.currentIndex?e("k-structure-form",{ref:"form",attrs:{fields:t.form,index:t.currentIndex,total:t.items.length},on:{close:t.onFormClose,discard:t.onFormDiscard,paginate:function(e){return t.onFormPaginate(e.offset)},submit:t.onFormSubmit},model:{value:t.currentModel,callback:function(e){t.currentModel=e},expression:"currentModel"}}):0===t.items.length?e("k-empty",{attrs:{"data-invalid":t.isInvalid,icon:"list-bullet"},on:{click:t.onAdd}},[t._v(" "+t._s(t.empty||t.$t("field.structure.empty"))+" ")]):[e("k-table",{attrs:{columns:t.columns,disabled:t.disabled,fields:t.fields,empty:t.$t("field.structure.empty"),index:t.index,options:t.options,rows:t.paginatedItems,sortable:t.isSortable,"data-invalid":t.isInvalid},on:{cell:function(e){return t.jump(e.rowIndex,e.columnIndex)},input:t.onInput,option:t.onOption}}),t.limit?e("k-pagination",t._b({on:{paginate:t.paginate}},"k-pagination",t.pagination,!1)):t._e(),t.disabled?t._e():e("k-dialog",{ref:"remove",attrs:{"submit-button":t.$t("delete"),theme:"negative"},on:{submit:t.onRemove}},[e("k-text",[t._v(t._s(t.$t("field.structure.delete.confirm")))])],1)]],2)}),[],!1,null,null,null,null).exports;const dn=It({mixins:[le,ce,As,Ys],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-tags-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"tags"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const pn=It({mixins:[le,ce,Is],inheritAttrs:!1,props:{icon:{type:String,default:"phone"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-tel-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"tel"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const hn=It({mixins:[le,ce,as,Ys],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()},select(){this.$refs.input.select()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-text-field",attrs:{input:t._uid,counter:t.counterOptions},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options")]},proxy:!0}],null,!0)},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const mn=It({mixins:[le,ce,Es,Ys],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-textarea-field",attrs:{input:t._uid,counter:t.counterOptions}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,type:"textarea",theme:"field"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const fn=It({mixins:[le,ce,js],inheritAttrs:!1,props:{icon:{type:String,default:"clock"},times:{type:Boolean,default:!0}},methods:{focus(){this.$refs.input.focus()},select(t){var e;this.$emit("input",t),null==(e=this.$refs.times)||e.close()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-time-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"time"},on:{input:function(e){return t.$emit("input",e||"")}},scopedSlots:t._u([t.times?{key:"icon",fn:function(){return[e("k-dropdown",[e("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon||"clock",tooltip:t.$t("time.select")},on:{click:function(e){return t.$refs.times.toggle()}}}),e("k-dropdown-content",{ref:"times",attrs:{align:"right"}},[e("k-times",{attrs:{display:t.display,value:t.value},on:{input:t.select}})],1)],1)]},proxy:!0}:null],null,!0)},"k-input",t.$props,!1))],1)}),[],!1,null,null,null,null).exports;const gn=It({mixins:[le,ce,Bs],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-toggle-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"toggle"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const kn=It({mixins:[le,ce,Ns],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()},onInput(t){this.$emit("input",t)}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-toggles-field"},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",class:{grow:t.grow},attrs:{id:t._uid,theme:"field",type:"toggles"}},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const bn=It({mixins:[le,ce,Fs],inheritAttrs:!1,props:{link:{type:Boolean,default:!0},icon:{type:String,default:"url"}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-url-field",attrs:{input:t._uid}},"k-field",t.$props,!1),[e("k-input",t._g(t._b({ref:"input",attrs:{id:t._uid,theme:"field",type:"url"},scopedSlots:t._u([{key:"icon",fn:function(){return[t.link?e("k-button",{staticClass:"k-input-icon-button",attrs:{icon:t.icon,link:t.value,tooltip:t.$t("open"),tabindex:"-1",target:"_blank"}}):t._e()]},proxy:!0}])},"k-input",t.$props,!1),t.$listeners))],1)}),[],!1,null,null,null,null).exports;const yn=It({mixins:[Gs],computed:{emptyProps(){return{icon:"users",text:this.empty||this.$t("field.users.empty")}}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-users-field",scopedSlots:t._u([{key:"options",fn:function(){return[e("k-button-group",{staticClass:"k-field-options"},[t.more&&!t.disabled?e("k-button",{staticClass:"k-field-options-button",attrs:{icon:t.btnIcon,text:t.btnLabel},on:{click:t.open}}):t._e()],1)]},proxy:!0}])},"k-field",t.$props,!1),[e("k-collection",t._b({on:{empty:t.open,sort:t.onInput,sortChange:function(e){return t.$emit("change",e)}},scopedSlots:t._u([{key:"options",fn:function({index:s}){return[t.disabled?t._e():e("k-button",{attrs:{tooltip:t.$t("remove"),icon:"remove"},on:{click:function(e){return t.remove(s)}}})]}}])},"k-collection",t.collection,!1)),e("k-users-dialog",{ref:"selector",on:{submit:t.select}})],1)}),[],!1,null,null,null,null).exports;const vn=It({mixins:[le,ce,Ve],inheritAttrs:!1,methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("k-field",t._b({staticClass:"k-writer-field",attrs:{input:t._uid,counter:!1}},"k-field",t.$props,!1),[e("k-input",t._b({attrs:{after:t.after,before:t.before,icon:t.icon,theme:"field"}},"k-input",t.$props,!1),[e("k-writer",t._b({ref:"input",staticClass:"k-writer-field-input",attrs:{value:t.value},on:{input:function(e){return t.$emit("input",e)}}},"k-writer",t.$props,!1))],1)],1)}),[],!1,null,null,null,null).exports;t.component("k-blocks-field",zs),t.component("k-checkboxes-field",Hs),t.component("k-date-field",Us),t.component("k-email-field",Ks),t.component("k-files-field",Js),t.component("k-gap-field",Vs),t.component("k-headline-field",Ws),t.component("k-info-field",Xs),t.component("k-layout-field",Zs),t.component("k-line-field",Qs),t.component("k-list-field",tn),t.component("k-multiselect-field",en),t.component("k-number-field",sn),t.component("k-pages-field",nn),t.component("k-password-field",on),t.component("k-radio-field",rn),t.component("k-range-field",ln),t.component("k-select-field",an),t.component("k-slug-field",un),t.component("k-structure-field",cn),t.component("k-tags-field",dn),t.component("k-text-field",hn),t.component("k-textarea-field",mn),t.component("k-tel-field",pn),t.component("k-time-field",fn),t.component("k-toggle-field",gn),t.component("k-toggles-field",kn),t.component("k-url-field",bn),t.component("k-users-field",yn),t.component("k-writer-field",vn);const $n=It({props:{cover:Boolean,ratio:String},computed:{ratioPadding(){return this.$helper.ratio(this.ratio)}}},(function(){var t=this;return(0,t._self._c)("span",{staticClass:"k-aspect-ratio",style:{"padding-bottom":t.ratioPadding},attrs:{"data-cover":t.cover}},[t._t("default")],2)}),[],!1,null,null,null,null).exports;const _n=It({},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-bar"},[t.$slots.left?e("div",{staticClass:"k-bar-slot",attrs:{"data-position":"left"}},[t._t("left")],2):t._e(),t.$slots.center?e("div",{staticClass:"k-bar-slot",attrs:{"data-position":"center"}},[t._t("center")],2):t._e(),t.$slots.right?e("div",{staticClass:"k-bar-slot",attrs:{"data-position":"right"}},[t._t("right")],2):t._e()])}),[],!1,null,null,null,null).exports;const xn=It({props:{theme:{type:String,default:"none"},text:String,html:{type:Boolean,default:!1}}},(function(){var t=this,e=t._self._c;return e("div",t._g({staticClass:"k-box",attrs:{"data-theme":t.theme}},t.$listeners),[t._t("default",(function(){return[t.html?e("k-text",{attrs:{html:t.text}}):e("k-text",[t._v(" "+t._s(t.text)+" ")])]}))],2)}),[],!1,null,null,null,null).exports;const wn=It({inheritAttrs:!1,props:{back:String,color:String,element:{type:String,default:"li"},image:Object,link:String,text:String}},(function(){var t=this,e=t._self._c;return e(t.link?"k-link":"p",{tag:"component",staticClass:"k-bubble",style:{color:t.$helper.color(t.color),background:t.$helper.color(t.back)},attrs:{to:t.link},nativeOn:{click:function(t){t.stopPropagation()}}},[t.image?e("k-item-image",{attrs:{image:t.image,layout:"list"}}):t._e(),t._v(" "+t._s(t.text)+" ")],1)}),[],!1,null,null,null,null).exports;const Sn=It({inheritAttrs:!1,props:{bubbles:Array},computed:{items(){let t=this.bubbles;return"string"==typeof t&&(t=t.split(",")),t.map((t=>"string"==typeof t?{text:t}:t))}}},(function(){var t=this,e=t._self._c;return e("ul",{staticClass:"k-bubbles"},t._l(t.items,(function(s,n){return e("li",{key:n},[e("k-bubble",t._b({},"k-bubble",s,!1))],1)})),0)}),[],!1,null,null,null,null).exports;const Cn=It({props:{columns:{type:[Object,Array],default:()=>({})},empty:Object,help:String,items:{type:[Array,Object],default:()=>[]},layout:{type:String,default:"list"},link:{type:Boolean,default:!0},size:String,sortable:Boolean,pagination:{type:[Boolean,Object],default:()=>!1}},computed:{hasPagination(){return!1!==this.pagination&&(!0!==this.paginationOptions.hide&&!(this.pagination.total<=this.pagination.limit))},hasFooter(){return!(!this.hasPagination&&!this.help)},paginationOptions(){return{limit:10,details:!0,keys:!1,total:0,hide:!1,..."object"!=typeof this.pagination?{}:this.pagination}}},watch:{$props(){this.$forceUpdate()}},methods:{onEmpty(t){t.stopPropagation(),this.$emit("empty")},onOption(...t){this.$emit("action",...t),this.$emit("option",...t)}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-collection"},[t.items.length?e("k-items",{attrs:{columns:t.columns,items:t.items,layout:t.layout,link:t.link,size:t.size,sortable:t.sortable},on:{change:function(e){return t.$emit("change",e)},item:function(e){return t.$emit("item",e)},option:t.onOption,sort:function(e){return t.$emit("sort",e)}},scopedSlots:t._u([{key:"options",fn:function({item:e,itemIndex:s}){return[t._t("options",null,null,{item:e,index:s})]}}],null,!0)}):e("k-empty",t._g(t._b({attrs:{layout:t.layout}},"k-empty",t.empty,!1),t.$listeners.empty?{click:t.onEmpty}:{})),t.hasFooter?e("footer",{staticClass:"k-collection-footer"},[t.help?e("k-text",{staticClass:"k-collection-help",attrs:{theme:"help",html:t.help}}):t._e(),e("div",{staticClass:"k-collection-pagination"},[t.hasPagination?e("k-pagination",t._b({on:{paginate:function(e){return t.$emit("paginate",e)}}},"k-pagination",t.paginationOptions,!1)):t._e()],1)],1):t._e()],1)}),[],!1,null,null,null,null).exports;const On=It({props:{width:String,sticky:Boolean}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-column",attrs:{"data-width":t.width,"data-sticky":t.sticky}},[e("div",[t._t("default")],2)])}),[],!1,null,null,null,null).exports;const An=It({props:{disabled:{type:Boolean,default:!1}},data:()=>({files:[],dragging:!1,over:!1}),methods:{cancel(){this.reset()},reset(){this.dragging=!1,this.over=!1},onDrop(t){return!0===this.disabled||!1===this.$helper.isUploadEvent(t)?this.reset():(this.$events.$emit("dropzone.drop"),this.files=t.dataTransfer.files,this.$emit("drop",this.files),void this.reset())},onEnter(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(this.dragging=!0)},onLeave(){this.reset()},onOver(t){!1===this.disabled&&this.$helper.isUploadEvent(t)&&(t.dataTransfer.dropEffect="copy",this.over=!0)}}},(function(){var t=this;return(0,t._self._c)("div",{staticClass:"k-dropzone",attrs:{"data-dragging":t.dragging,"data-over":t.over},on:{dragenter:t.onEnter,dragleave:t.onLeave,dragover:t.onOver,drop:t.onDrop}},[t._t("default")],2)}),[],!1,null,null,null,null).exports;const Tn=It({props:{text:String,icon:String,layout:{type:String,default:"list"}},computed:{element(){return void 0!==this.$listeners.click?"button":"div"}}},(function(){var t=this,e=t._self._c;return e(t.element,t._g({tag:"component",staticClass:"k-empty",attrs:{"data-layout":t.layout,type:"button"===t.element&&"button"}},t.$listeners),[t.icon?e("k-icon",{attrs:{type:t.icon}}):t._e(),e("p",[t._t("default",(function(){return[t._v(t._s(t.text))]}))],2)],1)}),[],!1,null,null,null,null).exports;const In=It({props:{details:Array,image:Object,url:String}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-file-preview"},[e("k-view",{staticClass:"k-file-preview-layout"},[e("div",{staticClass:"k-file-preview-image"},[e("k-link",{staticClass:"k-file-preview-image-link",attrs:{to:t.url,title:t.$t("open"),target:"_blank"}},[e("k-item-image",{attrs:{image:t.image,layout:"cards"}})],1)],1),e("div",{staticClass:"k-file-preview-details"},[e("ul",t._l(t.details,(function(s){return e("li",{key:s.title},[e("h3",[t._v(t._s(s.title))]),e("p",[s.link?e("k-link",{attrs:{to:s.link,tabindex:"-1",target:"_blank"}},[t._v(" /"+t._s(s.text)+" ")]):[t._v(" "+t._s(s.text)+" ")]],2)])})),0)])])],1)}),[],!1,null,null,null,null).exports;const Mn=It({props:{gutter:String}},(function(){var t=this;return(0,t._self._c)("div",{staticClass:"k-grid",attrs:{"data-gutter":t.gutter}},[t._t("default")],2)}),[],!1,null,null,null,null).exports;const En=It({props:{editable:Boolean,tab:String,tabs:{type:Array,default:()=>[]}},computed:{tabsWithBadges(){const t=Object.keys(this.$store.getters["content/changes"]());return this.tabs.map((e=>{let s=[];return Object.values(e.columns).forEach((t=>{Object.values(t.sections).forEach((t=>{"fields"===t.type&&Object.keys(t.fields).forEach((t=>{s.push(t)}))}))})),e.badge=s.filter((e=>t.includes(e.toLowerCase()))).length,e}))}}},(function(){var t=this,e=t._self._c;return e("header",{staticClass:"k-header",attrs:{"data-editable":t.editable,"data-tabs":t.tabsWithBadges.length>1}},[e("k-headline",{attrs:{tag:"h1",size:"huge"}},[t.editable&&t.$listeners.edit?e("span",{staticClass:"k-headline-editable",on:{click:function(e){return t.$emit("edit")}}},[t._t("default"),e("k-icon",{attrs:{type:"edit"}})],2):t._t("default")],2),t.$slots.left||t.$slots.right?e("k-bar",{staticClass:"k-header-buttons",scopedSlots:t._u([{key:"left",fn:function(){return[t._t("left")]},proxy:!0},{key:"right",fn:function(){return[t._t("right")]},proxy:!0}],null,!0)}):t._e(),e("k-tabs",{attrs:{tab:t.tab,tabs:t.tabsWithBadges,theme:"notice"}})],1)}),[],!1,null,null,null,null).exports;const Ln=It({inheritAttrs:!1},(function(){var t=this,e=t._self._c;return e("k-panel",{staticClass:"k-panel-inside",attrs:{tabindex:"0"}},[e("header",{staticClass:"k-panel-header"},[e("k-topbar",{attrs:{breadcrumb:t.$view.breadcrumb,license:t.$license,menu:t.$menu,view:t.$view}})],1),e("main",{staticClass:"k-panel-view scroll-y"},[t._t("default")],2),t._t("footer")],2)}),[],!1,null,null,null,null).exports;const jn=It({inheritAttrs:!1,props:{data:Object,flag:Object,image:[Object,Boolean],info:String,layout:{type:String,default:"list"},link:{type:[Boolean,String,Function]},options:{type:[Array,Function,String]},sortable:Boolean,target:String,text:String,width:String},computed:{hasFigure(){return!1!==this.image&&Object.keys(this.image).length>0},title(){return this.text||"-"}},methods:{onOption(t){this.$emit("action",t),this.$emit("option",t)}}},(function(){var t=this,e=t._self._c;return e("article",t._b({staticClass:"k-item",class:!!t.layout&&"k-"+t.layout+"-item",attrs:{"data-has-figure":t.hasFigure,"data-has-flag":Boolean(t.flag),"data-has-info":Boolean(t.info),"data-has-options":Boolean(t.options),tabindex:"-1"},on:{click:function(e){return t.$emit("click",e)},dragstart:function(e){return t.$emit("drag",e)}}},"article",t.data,!1),[t._t("image",(function(){return[t.hasFigure?e("k-item-image",{attrs:{image:t.image,layout:t.layout,width:t.width}}):t._e()]})),t.sortable?e("k-sort-handle",{staticClass:"k-item-sort-handle"}):t._e(),e("header",{staticClass:"k-item-content"},[t._t("default",(function(){return[e("h3",{staticClass:"k-item-title"},[!1!==t.link?e("k-link",{staticClass:"k-item-title-link",attrs:{target:t.target,to:t.link}},[e("span",{domProps:{innerHTML:t._s(t.title)}})]):e("span",{domProps:{innerHTML:t._s(t.title)}})],1),t.info?e("p",{staticClass:"k-item-info",domProps:{innerHTML:t._s(t.info)}}):t._e()]}))],2),t.flag||t.options||t.$slots.options?e("footer",{staticClass:"k-item-footer"},[e("nav",{staticClass:"k-item-buttons",on:{click:function(t){t.stopPropagation()}}},[t.flag?e("k-status-icon",t._b({},"k-status-icon",t.flag,!1)):t._e(),t._t("options",(function(){return[t.options?e("k-options-dropdown",{staticClass:"k-item-options-dropdown",attrs:{options:t.options},on:{option:t.onOption}}):t._e()]}))],2)]):t._e()],2)}),[],!1,null,null,null,null).exports;const Dn=It({inheritAttrs:!1,props:{image:[Object,Boolean],layout:{type:String,default:"list"},width:String},computed:{back(){return this.image.back||"black"},ratio(){return"cards"===this.layout&&this.image.ratio||"1/1"},size(){switch(this.layout){case"cards":return"large";case"cardlets":return"medium";default:return"regular"}},sizes(){switch(this.width){case"1/2":case"2/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 44em, 27em";case"1/3":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 29.333em, 27em";case"1/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 22em, 27em";case"2/3":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 27em, 27em";case"3/4":return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 66em, 27em";default:return"(min-width: 30em) and (max-width: 65em) 59em, (min-width: 65em) 88em, 27em"}}}},(function(){var t=this,e=t._self._c;return t.image?e("div",{staticClass:"k-item-figure",style:{background:t.$helper.color(t.back)}},[t.image.src?e("k-image",{staticClass:"k-item-image",attrs:{cover:t.image.cover,ratio:t.ratio,sizes:t.sizes,src:t.image.src,srcset:t.image.srcset}}):e("k-aspect-ratio",{attrs:{ratio:t.ratio}},[e("k-icon",{staticClass:"k-item-icon",attrs:{color:t.$helper.color(t.image.color),type:t.image.icon}})],1)],1):t._e()}),[],!1,null,null,null,null).exports;const Bn=It({inheritAttrs:!1,props:{columns:{type:[Object,Array],default:()=>({})},items:{type:Array,default:()=>[]},layout:{type:String,default:"list"},link:{type:Boolean,default:!0},image:{type:[Object,Boolean],default:()=>({})},sortable:Boolean,empty:{type:[String,Object]},size:{type:String,default:"default"}},computed:{dragOptions(){return{sort:this.sortable,disabled:!1===this.sortable,draggable:".k-draggable-item"}},table(){return{columns:this.columns,rows:this.items,sortable:this.sortable}}},methods:{onDragStart(t,e){this.$store.dispatch("drag",{type:"text",data:e})},onOption(t,e,s){this.$emit("option",t,e,s)},imageOptions(t){let e=this.image,s=t.image;return!1!==e&&!1!==s&&("object"!=typeof e&&(e={}),"object"!=typeof s&&(s={}),{...s,...e})}}},(function(){var t=this,e=t._self._c;return"table"===t.layout?e("k-table",t._b({on:{change:function(e){return t.$emit("change",e)},sort:function(e){return t.$emit("sort",e)},option:t.onOption}},"k-table",t.table,!1)):e("k-draggable",{staticClass:"k-items",class:"k-"+t.layout+"-items",attrs:{handle:!0,options:t.dragOptions,"data-layout":t.layout,"data-size":t.size,list:t.items},on:{change:function(e){return t.$emit("change",e)},end:function(e){return t.$emit("sort",t.items,e)}}},t._l(t.items,(function(s,n){return e("k-item",t._b({key:s.id||n,class:{"k-draggable-item":t.sortable&&s.sortable},attrs:{image:t.imageOptions(s),layout:t.layout,link:!!t.link&&s.link,sortable:t.sortable&&s.sortable,width:s.column},on:{click:function(e){return t.$emit("item",s,n)},drag:function(e){return t.onDragStart(e,s.dragText)},option:function(e){return t.onOption(e,s,n)}},nativeOn:{mouseover:function(e){return t.$emit("hover",e,s,n)}},scopedSlots:t._u([{key:"options",fn:function(){return[t._t("options",null,null,{item:s,itemIndex:n})]},proxy:!0}],null,!0)},"k-item",s,!1))})),1)}),[],!1,null,null,null,null).exports;const Pn=It({inheritAttrs:!0,props:{autofocus:{type:Boolean,default:!0},centered:{type:Boolean,default:!1},dimmed:{type:Boolean,default:!0},loading:{type:Boolean,default:!1}},data:()=>({isOpen:!1,scrollTop:0}),methods:{close(){!1!==this.isOpen&&(this.isOpen=!1,this.$emit("close"),this.restoreScrollPosition(),this.$events.$off("keydown.esc",this.close))},focus(){var t,e;let s=this.$refs.overlay.querySelector("\n [autofocus],\n [data-autofocus]\n ");return null===s&&(s=this.$refs.overlay.querySelector("\n input,\n textarea,\n select,\n button\n ")),"function"==typeof(null==s?void 0:s.focus)?s.focus():"function"==typeof(null==(e=null==(t=this.$slots.default[0])?void 0:t.context)?void 0:e.focus)?this.$slots.default[0].context.focus():void 0},open(){!0!==this.isOpen&&(this.storeScrollPosition(),this.isOpen=!0,this.$emit("open"),this.$events.$on("keydown.esc",this.close),setTimeout((()=>{!0===this.autofocus&&this.focus(),document.querySelector(".k-overlay > *").addEventListener("mousedown",(t=>t.stopPropagation())),this.$emit("ready")}),1))},restoreScrollPosition(){const t=document.querySelector(".k-panel-view");(null==t?void 0:t.scrollTop)&&(t.scrollTop=this.scrollTop)},storeScrollPosition(){const t=document.querySelector(".k-panel-view");(null==t?void 0:t.scrollTop)?this.scrollTop=t.scrollTop:this.scrollTop=0}}},(function(){var t=this,e=t._self._c;return t.isOpen?e("portal",[e("div",t._g({ref:"overlay",staticClass:"k-overlay",class:t.$vnode.data.staticClass,attrs:{"data-centered":t.loading||t.centered,"data-dimmed":t.dimmed,"data-loading":t.loading,dir:t.$translation.direction},on:{mousedown:t.close}},t.$listeners),[t.loading?e("k-loader",{staticClass:"k-overlay-loader"}):t._t("default",null,{close:t.close,isOpen:t.isOpen})],2)]):t._e()}),[],!1,null,null,null,null).exports;const Nn=It({computed:{defaultLanguage(){return!!this.$language&&this.$language.default},dialog(){return this.$helper.clone(this.$store.state.dialog)},dir(){return this.$translation.direction},language(){return this.$language?this.$language.code:null},role(){return this.$user?this.$user.role:null},user(){return this.$user?this.$user.id:null}},watch:{dir:{handler(){document.body.dir=this.dir},immediate:!0}},created(){this.$events.$on("drop",this.drop)},destroyed(){this.$events.$off("drop",this.drop)},methods:{drop(){this.$store.dispatch("drag",null)}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-panel",attrs:{"data-dragging":t.$store.state.drag,"data-loading":t.$store.state.isLoading,"data-language":t.language,"data-language-default":t.defaultLanguage,"data-role":t.role,"data-translation":t.$translation.code,"data-user":t.user,dir:t.dir}},[t._t("default"),t.$store.state.dialog&&t.$store.state.dialog.props?[e("k-fiber-dialog",t._b({},"k-fiber-dialog",t.dialog,!1))]:t._e(),!1!==t.$store.state.fatal?e("k-fatal",{attrs:{html:t.$store.state.fatal}}):t._e(),!1===t.$system.isLocal?e("k-offline-warning"):t._e(),e("k-icons")],2)}),[],!1,null,null,null,null).exports;const qn=It({props:{reports:Array,size:{type:String,default:"large"}}},(function(){var t=this,e=t._self._c;return e("dl",{staticClass:"k-stats",attrs:{"data-size":t.size}},t._l(t.reports,(function(s,n){return e(s.link?"k-link":"div",{key:n,tag:"component",staticClass:"k-stat",attrs:{"data-theme":s.theme,"data-click":!!s.click,to:s.link},on:{click:function(t){s.click&&s.click()}}},[e("dt",{staticClass:"k-stat-label"},[t._v(t._s(s.label))]),e("dd",{staticClass:"k-stat-value"},[t._v(t._s(s.value))]),e("dd",{staticClass:"k-stat-info"},[t._v(t._s(s.info))])])})),1)}),[],!1,null,null,null,null).exports;const Fn=It({inheritAttrs:!1,props:{columns:Object,disabled:Boolean,fields:{type:Object,default:()=>({})},empty:String,index:{type:[Number,Boolean],default:1},rows:Array,options:[Array,Function],sortable:Boolean},data(){return{values:this.rows}},computed:{columnsCount(){return Object.keys(this.columns).length},dragOptions(){return{disabled:!this.sortable,fallbackClass:"k-table-row-fallback",ghostClass:"k-table-row-ghost"}},hasIndexColumn(){return this.sortable||!1!==this.index},hasOptions(){return this.options||Object.values(this.values).filter((t=>t.options)).length>0}},watch:{rows(){this.values=this.rows}},methods:{isColumnEmpty(t){return 0===this.rows.filter((e=>!1===this.$helper.object.isEmpty(e[t]))).length},label(t,e){return t.label||this.$helper.string.ucfirst(e)},onChange(t){this.$emit("change",t)},onCell(t){this.$emit("cell",t)},onCellUpdate({columnIndex:t,rowIndex:e,value:s}){this.values[e][t]=s,this.$emit("input",this.values)},onHeader(t){this.$emit("header",t)},onOption(t,e,s){this.$emit("option",t,e,s)},onSort(){this.$emit("input",this.values),this.$emit("sort",this.values)},width(t){return"string"!=typeof t?"auto":!1===t.includes("/")?t:this.$helper.ratio(t,"auto",!1)}}},(function(){var t=this,e=t._self._c;return e("table",{staticClass:"k-table",attrs:{"data-disabled":t.disabled,"data-indexed":t.hasIndexColumn}},[e("thead",[e("tr",[t.hasIndexColumn?e("th",{staticClass:"k-table-index-column",attrs:{"data-mobile":""}},[t._v(" # ")]):t._e(),t._l(t.columns,(function(s,n){return e("th",{key:n+"-header",staticClass:"k-table-column",style:"width:"+t.width(s.width),attrs:{"data-mobile":s.mobile},on:{click:function(e){return t.onHeader({column:s,columnIndex:n})}}},[t._t("header",(function(){return[t._v(" "+t._s(t.label(s,n))+" ")]}),null,{column:s,columnIndex:n,label:t.label(s,n)})],2)})),t.hasOptions?e("th",{staticClass:"k-table-options-column",attrs:{"data-mobile":""}}):t._e()],2)]),e("k-draggable",{attrs:{list:t.values,options:t.dragOptions,handle:!0,element:"tbody"},on:{change:t.onChange,end:t.onSort}},[0===t.rows.length?e("tr",[e("td",{staticClass:"k-table-empty",attrs:{colspan:t.columnsCount}},[t._v(" "+t._s(t.empty)+" ")])]):t._l(t.values,(function(s,n){return e("tr",{key:n},[t.hasIndexColumn?e("td",{staticClass:"k-table-index-column",attrs:{"data-sortable":t.sortable&&!1!==s.sortable,"data-mobile":""}},[t._t("index",(function(){return[e("div",{staticClass:"k-table-index",domProps:{textContent:t._s(t.index+n)}})]}),null,{row:s,rowIndex:n}),t.sortable&&!1!==s.sortable?e("k-sort-handle",{staticClass:"k-table-sort-handle"}):t._e()],2):t._e(),t._l(t.columns,(function(i,o){return e("k-table-cell",{key:n+"-"+o,staticClass:"k-table-column",style:"width:"+t.width(i.width),attrs:{column:i,field:t.fields[o],row:s,mobile:i.mobile,value:s[o]},on:{input:function(e){return t.onCellUpdate({columnIndex:o,rowIndex:n,value:e})}},nativeOn:{click:function(e){return t.onCell({row:s,rowIndex:n,column:i,columnIndex:o})}}})})),t.hasOptions?e("td",{staticClass:"k-table-options-column",attrs:{"data-mobile":""}},[t._t("options",(function(){return[e("k-options-dropdown",{attrs:{options:s.options||t.options,text:(s.options||t.options).length>1},on:{option:function(e){return t.onOption(e,s,n)}}})]}),null,{row:s,rowIndex:n,options:t.options})],2):t._e()],2)}))],2)],1)}),[],!1,null,null,null,null).exports;const Rn=It({inheritAttrs:!1,props:{column:Object,field:Object,mobile:{type:Boolean,default:!1},row:Object,value:{default:""}},computed:{component(){return this.$helper.isComponent(`k-${this.type}-field-preview`)?`k-${this.type}-field-preview`:this.$helper.isComponent(`k-table-${this.type}-cell`)?`k-table-${this.type}-cell`:Array.isArray(this.value)?"k-array-field-preview":"k-text-field-preview"},type(){var t;return this.column.type||(null==(t=this.field)?void 0:t.type)}}},(function(){var t=this,e=t._self._c;return e("td",{attrs:{"data-align":t.column.align,"data-mobile":t.mobile}},[!1===t.$helper.object.isEmpty(t.value)?[e(t.component,{tag:"component",attrs:{column:t.column,field:t.field,row:t.row,value:t.value},on:{input:function(e){return t.$emit("input",e)}}})]:t._e()],2)}),[],!1,null,null,null,null).exports;const zn=It({props:{tab:String,tabs:Array,theme:String},data(){return{size:null,visibleTabs:this.tabs,invisibleTabs:[]}},computed:{current(){return(this.tabs.find((t=>t.name===this.tab))||this.tabs[0]||{}).name}},watch:{tabs:{handler(t){this.visibleTabs=t,this.invisibleTabs=[],this.resize(!0)},immediate:!0}},created(){window.addEventListener("resize",this.resize)},destroyed(){window.removeEventListener("resize",this.resize)},methods:{resize(t){if(this.tabs&&!(this.tabs.length<=1)){if(this.tabs.length<=3)return this.visibleTabs=this.tabs,void(this.invisibleTabs=[]);if(window.innerWidth>=700){if("large"===this.size&&!t)return;this.visibleTabs=this.tabs,this.invisibleTabs=[],this.size="large"}else{if("small"===this.size&&!t)return;this.visibleTabs=this.tabs.slice(0,2),this.invisibleTabs=this.tabs.slice(2),this.size="small"}}}}},(function(){var t=this,e=t._self._c;return t.tabs&&t.tabs.length>1?e("div",{staticClass:"k-tabs",attrs:{"data-theme":t.theme}},[e("nav",[t._l(t.visibleTabs,(function(s){return e("k-button",{key:s.name,staticClass:"k-tab-button",attrs:{link:s.link,current:t.current===s.name,icon:s.icon,tooltip:s.label}},[t._v(" "+t._s(s.label||s.text||s.name)+" "),s.badge?e("span",{staticClass:"k-tabs-badge"},[t._v(" "+t._s(s.badge)+" ")]):t._e()])})),t.invisibleTabs.length?e("k-button",{staticClass:"k-tab-button k-tabs-dropdown-button",attrs:{text:t.$t("more"),icon:"dots"},on:{click:function(e){return e.stopPropagation(),t.$refs.more.toggle()}}}):t._e()],2),t.invisibleTabs.length?e("k-dropdown-content",{ref:"more",staticClass:"k-tabs-dropdown",attrs:{align:"right"}},t._l(t.invisibleTabs,(function(s){return e("k-dropdown-item",{key:"more-"+s.name,attrs:{link:s.link,current:t.tab===s.name,icon:s.icon,tooltip:s.label}},[t._v(" "+t._s(s.label||s.text||s.name)+" ")])})),1):t._e()],1):t._e()}),[],!1,null,null,null,null).exports;const Yn=It({props:{align:String}},(function(){var t=this;return(0,t._self._c)("div",{staticClass:"k-view",attrs:{"data-align":t.align}},[t._t("default")],2)}),[],!1,null,null,null,null).exports,Hn={};const Un=It({components:{draggable:()=>{return t=()=>import("./vuedraggable.js"),(e=[])&&0!==e.length?Promise.all(e.map((t=>{if((t=function(t){return"/"+t}(t))in Hn)return;Hn[t]=!0;const e=t.endsWith(".css"),s=e?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${t}"]${s}`))return;const n=document.createElement("link");return n.rel=e?"stylesheet":"modulepreload",e||(n.as="script",n.crossOrigin=""),n.href=t,document.head.appendChild(n),e?new Promise(((e,s)=>{n.addEventListener("load",e),n.addEventListener("error",(()=>s(new Error(`Unable to preload CSS for ${t}`))))})):void 0}))).then((()=>t())):t();var t,e}},props:{data:Object,element:String,handle:[String,Boolean],list:[Array,Object],move:Function,options:Object},data(){return{listeners:{...this.$listeners,start:t=>{this.$store.dispatch("drag",{}),this.$listeners.start&&this.$listeners.start(t)},end:t=>{this.$store.dispatch("drag",null),this.$listeners.end&&this.$listeners.end(t)}}}},computed:{dragOptions(){let t=!1;return t=!0===this.handle?".k-sort-handle":this.handle,{fallbackClass:"k-sortable-fallback",fallbackOnBody:!0,forceFallback:!0,ghostClass:"k-sortable-ghost",handle:t,scroll:document.querySelector(".k-panel-view"),...this.options}}}},(function(){var t=this;return(0,t._self._c)("draggable",t._g(t._b({staticClass:"k-draggable",attrs:{"component-data":t.data,tag:t.element,list:t.list,move:t.move},scopedSlots:t._u([{key:"footer",fn:function(){return[t._t("footer")]},proxy:!0}],null,!0)},"draggable",t.dragOptions,!1),t.listeners),[t._t("default")],2)}),[],!1,null,null,null,null).exports;const Kn=It({data:()=>({error:null}),errorCaptured(t){return this.$config.debug&&window.console.warn(t),this.error=t,!1},render(t){return this.error?this.$slots.error?this.$slots.error[0]:this.$scopedSlots.error?this.$scopedSlots.error({error:this.error}):t("k-box",{attrs:{theme:"negative"}},this.error.message||this.error):this.$slots.default[0]}},null,null,!1,null,null,null,null).exports;const Gn=It({props:{html:String},mounted(){try{let t=this.$refs.iframe.contentWindow.document;t.open(),t.write(this.html),t.close()}catch(t){console.error(t)}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-fatal"},[e("div",{staticClass:"k-fatal-box"},[e("k-bar",{scopedSlots:t._u([{key:"left",fn:function(){return[e("k-headline",[t._v(" The JSON response could not be parsed ")])]},proxy:!0},{key:"right",fn:function(){return[e("k-button",{attrs:{icon:"cancel",text:"Close"},on:{click:function(e){return t.$store.dispatch("fatal",!1)}}})]},proxy:!0}])}),e("iframe",{ref:"iframe",staticClass:"k-fatal-iframe"})],1)])}),[],!1,null,null,null,null).exports;const Jn=It({props:{link:String,size:{type:String},tag:{type:String,default:"h2"},theme:{type:String}}},(function(){var t=this,e=t._self._c;return e(t.tag,t._g({tag:"component",staticClass:"k-headline",attrs:{"data-theme":t.theme,"data-size":t.size}},t.$listeners),[t.link?e("k-link",{attrs:{to:t.link}},[t._t("default")],2):t._t("default")],2)}),[],!1,null,null,null,null).exports;const Vn=It({props:{alt:String,color:String,back:String,size:String,type:String},computed:{isEmoji(){return this.$helper.string.hasEmoji(this.type)}}},(function(){var t=this,e=t._self._c;return e("span",{class:"k-icon k-icon-"+t.type,style:{background:t.$helper.color(t.back)},attrs:{"aria-label":t.alt,role:t.alt?"img":null,"aria-hidden":!t.alt,"data-back":t.back,"data-size":t.size}},[t.isEmoji?e("span",{staticClass:"k-icon-emoji"},[t._v(t._s(t.type))]):e("svg",{style:{color:t.$helper.color(t.color)},attrs:{viewBox:"0 0 16 16"}},[e("use",{attrs:{"xlink:href":"#icon-"+t.type}})])])}),[],!1,null,null,null,null).exports;const Wn=It({icons:window.panel.plugins.icons},(function(){var t=this,e=t._self._c;return e("svg",{staticClass:"k-icons",attrs:{"aria-hidden":"true",xmlns:"http://www.w3.org/2000/svg",overflow:"hidden"}},[e("defs",t._l(t.$options.icons,(function(s,n){return e("symbol",{key:n,attrs:{id:"icon-"+n,viewBox:"0 0 16 16"},domProps:{innerHTML:t._s(s)}})})),0)])}),[],!1,null,null,null,null).exports;const Xn=It({props:{alt:String,back:String,cover:Boolean,ratio:String,sizes:String,src:String,srcset:String},data:()=>({loaded:{type:Boolean,default:!1},error:{type:Boolean,default:!1}}),computed:{ratioPadding(){return this.$helper.ratio(this.ratio||"1/1")}},created(){let t=new Image;t.onload=()=>{this.loaded=!0,this.$emit("load")},t.onerror=()=>{this.error=!0,this.$emit("error")},t.src=this.src}},(function(){var t=this,e=t._self._c;return e("span",t._g({staticClass:"k-image",attrs:{"data-ratio":t.ratio,"data-back":t.back,"data-cover":t.cover}},t.$listeners),[e("span",{style:"padding-bottom:"+t.ratioPadding},[t.loaded?e("img",{key:t.src,attrs:{alt:t.alt||"",src:t.src,srcset:t.srcset,sizes:t.sizes},on:{dragstart:function(t){t.preventDefault()}}}):t._e(),t.loaded||t.error?t._e():e("k-loader",{attrs:{position:"center",theme:"light"}}),!t.loaded&&t.error?e("k-icon",{staticClass:"k-image-error",attrs:{type:"cancel"}}):t._e()],1)])}),[],!1,null,null,null,null).exports;const Zn=It({},(function(){var t=this._self._c;return t("span",{staticClass:"k-loader"},[t("k-icon",{staticClass:"k-loader-icon",attrs:{type:"loader"}})],1)}),[],!1,null,null,null,null).exports;const Qn=It({data:()=>({offline:!1}),created(){this.$events.$on("offline",this.isOffline),this.$events.$on("online",this.isOnline)},destroyed(){this.$events.$off("offline",this.isOffline),this.$events.$off("online",this.isOnline)},methods:{isOnline(){this.offline=!1},isOffline(){this.offline=!0}}},(function(){var t=this,e=t._self._c;return t.offline?e("div",{staticClass:"k-offline-warning"},[e("p",[e("k-icon",{attrs:{type:"bolt"}}),t._v(" "+t._s(t.$t("error.offline")))],1)]):t._e()}),[],!1,null,null,null,null).exports,ti=(t,e=!1)=>{if(t>=0&&t<=100)return!0;if(e)throw new Error("value has to be between 0 and 100");return!1};const ei=It({props:{value:{type:Number,default:0,validator:ti}},data(){return{state:this.value}},watch:{value(t){this.state=t}},methods:{set(t){ti(t,!0),this.state=t}}},(function(){var t=this;return(0,t._self._c)("progress",{staticClass:"k-progress",attrs:{max:"100"},domProps:{value:t.state}},[t._v(t._s(t.state)+"%")])}),[],!1,null,null,null,null).exports;const si=It({},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-registration"},[e("p",[t._v(t._s(t.$t("license.unregistered")))]),e("k-button",{staticClass:"k-topbar-button",attrs:{responsive:!0,tooltip:t.$t("license.unregistered"),icon:"key"},on:{click:function(e){return t.$dialog("registration")}}},[t._v(" "+t._s(t.$t("license.register"))+" ")]),e("k-button",{staticClass:"k-topbar-button",attrs:{responsive:!0,link:"https://getkirby.com/buy",target:"_blank",icon:"cart"}},[t._v(" "+t._s(t.$t("license.buy"))+" ")])],1)}),[],!1,null,null,null,null).exports;const ni=It({props:{icon:{type:String,default:"sort"}}},(function(){return(0,this._self._c)("k-icon",{staticClass:"k-sort-handle",attrs:{type:this.icon,"aria-hidden":"true"}})}),[],!1,null,null,null,null).exports;const ii=It({props:{click:{type:Function,default:()=>{}},disabled:Boolean,responsive:Boolean,status:String,text:String,tooltip:String},computed:{icon(){return"draft"===this.status?"circle-outline":"unlisted"===this.status?"circle-half":"circle"},theme(){return"draft"===this.status?"negative":"unlisted"===this.status?"info":"positive"},title(){let t=this.tooltip||this.text;return this.disabled&&(t+=` (${this.$t("disabled")})`),t}},methods:{onClick(){this.click(),this.$emit("click")}}},(function(){var t=this;return(0,t._self._c)("k-button",{class:"k-status-icon k-status-icon-"+t.status,attrs:{disabled:t.disabled,icon:t.icon,responsive:t.responsive,text:t.text,theme:t.theme,tooltip:t.title},on:{click:t.onClick}})}),[],!1,null,null,null,null).exports;const oi=It({props:{align:String,html:String,size:String,theme:String},computed:{attrs(){return{class:"k-text","data-align":this.align,"data-size":this.size,"data-theme":this.theme}}}},(function(){var t=this,e=t._self._c;return t.html?e("div",t._b({domProps:{innerHTML:t._s(t.html)}},"div",t.attrs,!1)):e("div",t._b({},"div",t.attrs,!1),[t._t("default")],2)}),[],!1,null,null,null,null).exports;const ri=It({props:{user:[Object,String]}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-user-info"},[t.user.avatar?e("k-image",{attrs:{cover:!0,src:t.user.avatar.url,ratio:"1/1"}}):e("k-icon",{attrs:{type:"user"}}),t._v(" "+t._s(t.user.name||t.user.email||t.user)+" ")],1)}),[],!1,null,null,null,null).exports;const li=It({props:{crumbs:{type:Array,default:()=>[]},label:{type:String,default:"Breadcrumb"},view:Object},computed:{dropdown(){return this.segments.map((t=>({...t,text:t.label,icon:"angle-right"})))},segments(){return[{link:this.view.link,label:this.view.breadcrumbLabel,icon:this.view.icon,loading:this.$store.state.isLoading},...this.crumbs]}},methods:{isLast(t){return this.crumbs.length-1===t}}},(function(){var t=this,e=t._self._c;return e("nav",{staticClass:"k-breadcrumb",attrs:{"aria-label":t.label}},[e("k-dropdown",{staticClass:"k-breadcrumb-dropdown"},[e("k-button",{attrs:{icon:"road-sign"},on:{click:function(e){return t.$refs.dropdown.toggle()}}}),e("k-dropdown-content",{ref:"dropdown",attrs:{options:t.dropdown,theme:"light"}})],1),e("ol",t._l(t.segments,(function(s,n){return e("li",{key:n},[e("k-link",{staticClass:"k-breadcrumb-link",attrs:{title:s.text||s.label,to:s.link,"aria-current":!!t.isLast(n)&&"page"}},[s.loading?e("k-loader",{staticClass:"k-breadcrumb-icon"}):s.icon?e("k-icon",{staticClass:"k-breadcrumb-icon",attrs:{type:s.icon}}):t._e(),e("span",{staticClass:"k-breadcrumb-link-text"},[t._v(" "+t._s(s.text||s.label)+" ")])],1)],1)})),0)],1)}),[],!1,null,null,null,null).exports;const ai=It({inheritAttrs:!1,props:{autofocus:Boolean,click:Function,current:[String,Boolean],disabled:Boolean,icon:String,id:[String,Number],link:String,responsive:Boolean,rel:String,role:String,target:String,tabindex:String,text:[String,Number],theme:String,tooltip:String,type:{type:String,default:"button"}},computed:{component(){return!0===this.disabled?"k-button-disabled":this.link?"k-button-link":"k-button-native"}},methods:{focus(){this.$refs.button.focus&&this.$refs.button.focus()},tab(){this.$refs.button.tab&&this.$refs.button.tab()},untab(){this.$refs.button.untab&&this.$refs.button.untab()}}},(function(){var t=this;return(0,t._self._c)(t.component,t._g(t._b({ref:"button",tag:"component"},"component",t.$props,!1),t.$listeners),[t.text?[t._v(" "+t._s(t.text)+" ")]:t._t("default")],2)}),[],!1,null,null,null,null).exports;const ui=It({inheritAttrs:!1,props:{icon:String,id:[String,Number],responsive:Boolean,theme:String,tooltip:String}},(function(){var t=this,e=t._self._c;return e("span",{staticClass:"k-button",attrs:{id:t.id,"data-disabled":!0,"data-responsive":t.responsive,"data-theme":t.theme,title:t.tooltip}},[t.icon?e("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?e("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)}),[],!1,null,null,null,null).exports;const ci=It({props:{buttons:Array}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-button-group"},[t.$slots.default?t._t("default"):t._l(t.buttons,(function(s,n){return e("k-button",t._b({key:n},"k-button",s,!1))}))],2)}),[],!1,null,null,null,null).exports;const di=It({inheritAttrs:!1,props:{autofocus:Boolean,current:[String,Boolean],icon:String,id:[String,Number],link:String,rel:String,responsive:Boolean,role:String,target:String,tabindex:String,theme:String,tooltip:String},methods:{focus(){this.$el.focus()}}},(function(){var t=this,e=t._self._c;return e("k-link",t._g({staticClass:"k-button",attrs:{id:t.id,"aria-current":t.current,autofocus:t.autofocus,"data-theme":t.theme,"data-responsive":t.responsive,rel:t.rel,role:t.role,tabindex:t.tabindex,target:t.target,title:t.tooltip,to:t.link}},t.$listeners),[t.icon?e("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?e("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)}),[],!1,null,null,null,null).exports,pi={mounted(){this.$el.addEventListener("keyup",this.onTab,!0),this.$el.addEventListener("blur",this.onUntab,!0)},destroyed(){this.$el.removeEventListener("keyup",this.onTab,!0),this.$el.removeEventListener("blur",this.onUntab,!0)},methods:{focus(){this.$el.focus&&this.$el.focus()},onTab(t){9===t.keyCode&&this.$el.setAttribute("data-tabbed",!0)},onUntab(){this.$el.removeAttribute("data-tabbed")},tab(){this.$el.focus(),this.$el.setAttribute("data-tabbed",!0)},untab(){this.$el.removeAttribute("data-tabbed")}}};const hi=It({mixins:[pi],inheritAttrs:!1,props:{autofocus:Boolean,click:{type:Function,default:()=>{}},current:[String,Boolean],icon:String,id:[String,Number],responsive:Boolean,role:String,tabindex:String,theme:String,tooltip:String,type:{type:String,default:"button"}}},(function(){var t=this,e=t._self._c;return e("button",t._g({staticClass:"k-button",attrs:{id:t.id,"aria-current":t.current,autofocus:t.autofocus,"data-theme":t.theme,"data-responsive":t.responsive,role:t.role,tabindex:t.tabindex,title:t.tooltip,type:t.type},on:{click:t.click}},t.$listeners),[t.icon?e("k-icon",{staticClass:"k-button-icon",attrs:{type:t.icon,alt:t.tooltip}}):t._e(),t.$slots.default?e("span",{staticClass:"k-button-text"},[t._t("default")],2):t._e()],1)}),[],!1,null,null,null,null).exports;const mi=It({},(function(){return(0,this._self._c)("span",{staticClass:"k-dropdown",on:{click:function(t){t.stopPropagation()}}},[this._t("default")],2)}),[],!1,null,null,null,null).exports;let fi=null;const gi=It({props:{align:{type:String,default:"left"},options:[Array,Function,String],theme:{type:String,default:"dark"}},data:()=>({current:-1,dropup:!1,isOpen:!1,items:[]}),methods:{async fetchOptions(t){if(!this.options)return t(this.items);"string"==typeof this.options?this.$dropdown(this.options)(t):"function"==typeof this.options?this.options(t):Array.isArray(this.options)&&t(this.options)},onOptionClick(t){"function"==typeof t.click?t.click.call(this):t.click&&this.$emit("action",t.click)},open(){this.reset(),fi&&fi!==this&&fi.close(),this.fetchOptions((t=>{this.$events.$on("keydown",this.navigate),this.$events.$on("click",this.close),this.items=t,this.isOpen=!0,fi=this,this.onOpen(),this.$emit("open")}))},reset(){this.current=-1,this.$events.$off("keydown",this.navigate),this.$events.$off("click",this.close)},close(){this.reset(),this.isOpen=fi=!1,this.$emit("close")},toggle(){this.isOpen?this.close():this.open()},focus(t=0){var e;(null==(e=this.$children[t])?void 0:e.focus)&&(this.current=t,this.$children[t].focus())},onOpen(){this.dropup=!1,this.$nextTick((()=>{if(this.$el){let t=window.innerHeight||document.body.clientHeight||document.documentElement.clientHeight,e=50,s=this.$el.getBoundingClientRect().top||0,n=this.$el.clientHeight;s+n>t-e&&n+2*ethis.$children.length-1){const t=this.$children.filter((t=>!1===t.disabled));this.current=this.$children.indexOf(t[t.length-1]);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled){this.focus(this.current);break}}break;case"Tab":for(;;){if(this.current++,this.current>this.$children.length-1){this.close(),this.$emit("leave",t.code);break}if(this.$children[this.current]&&!1===this.$children[this.current].disabled)break}}}}},(function(){var t=this,e=t._self._c;return t.isOpen?e("div",{staticClass:"k-dropdown-content",attrs:{"data-align":t.align,"data-dropup":t.dropup,"data-theme":t.theme}},[t._t("default",(function(){return[t._l(t.items,(function(s,n){return["-"===s?e("hr",{key:t._uid+"-item-"+n}):e("k-dropdown-item",t._b({key:t._uid+"-item-"+n,ref:t._uid+"-item-"+n,refInFor:!0,on:{click:function(e){return t.onOptionClick(s)}}},"k-dropdown-item",s,!1),[t._v(" "+t._s(s.text)+" ")])]}))]}))],2):t._e()}),[],!1,null,null,null,null).exports;const ki=It({inheritAttrs:!1,props:{disabled:Boolean,icon:String,image:[String,Object],link:String,target:String,theme:String,upload:String,current:[String,Boolean]},data(){return{listeners:{...this.$listeners,click:t=>{this.$parent.close(),this.$emit("click",t)}}}},methods:{focus(){this.$refs.button.focus()},tab(){this.$refs.button.tab()}}},(function(){var t=this;return(0,t._self._c)("k-button",t._g(t._b({ref:"button",staticClass:"k-dropdown-item"},"k-button",t.$props,!1),t.listeners),[t._t("default")],2)}),[],!1,null,null,null,null).exports;const bi=It({mixins:[pi],props:{disabled:Boolean,rel:String,tabindex:[String,Number],target:String,title:String,to:[String,Function]},data(){return{relAttr:"_blank"===this.target?"noreferrer noopener":this.rel,listeners:{...this.$listeners,click:this.onClick}}},computed:{href(){return"function"==typeof this.to?"":"/"!==this.to[0]||this.target?!0===this.to.includes("@")&&!1===this.to.includes("/")?"mailto:"+this.to:this.to:this.$url(this.to)}},methods:{isRoutable(t){if(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey)return!1;if(t.defaultPrevented)return!1;if(void 0!==t.button&&0!==t.button)return!1;if(this.target)return!1;if("string"==typeof this.href){if(this.href.includes("://")||this.href.startsWith("//"))return!1;if(this.href.includes("mailto:"))return!1}return!0},onClick(t){if(!0===this.disabled)return t.preventDefault(),!1;"function"==typeof this.to&&(t.preventDefault(),this.to()),this.isRoutable(t)&&(t.preventDefault(),this.$go(this.to)),this.$emit("click",t)}}},(function(){var t=this,e=t._self._c;return t.to&&!t.disabled?e("a",t._g({ref:"link",staticClass:"k-link",attrs:{href:t.href,rel:t.relAttr,tabindex:t.tabindex,target:t.target,title:t.title}},t.listeners),[t._t("default")],2):e("span",{staticClass:"k-link",attrs:{title:t.title,"data-disabled":""}},[t._t("default")],2)}),[],!1,null,null,null,null).exports;const yi=It({computed:{defaultLanguage(){return this.$languages.find((t=>!0===t.default))},language(){return this.$language},languages(){return this.$languages.filter((t=>!1===t.default))}},methods:{change(t){this.$emit("change",t),this.$go(window.location,{query:{language:t.code}})}}},(function(){var t=this,e=t._self._c;return t.languages.length?e("k-dropdown",{staticClass:"k-languages-dropdown"},[e("k-button",{attrs:{text:t.language.name,responsive:!0,icon:"globe"},on:{click:function(e){return t.$refs.languages.toggle()}}}),t.languages?e("k-dropdown-content",{ref:"languages"},[e("k-dropdown-item",{on:{click:function(e){return t.change(t.defaultLanguage)}}},[t._v(" "+t._s(t.defaultLanguage.name)+" ")]),e("hr"),t._l(t.languages,(function(s){return e("k-dropdown-item",{key:s.code,on:{click:function(e){return t.change(s)}}},[t._v(" "+t._s(s.name)+" ")])}))],2):t._e()],1):t._e()}),[],!1,null,null,null,null).exports;const vi=It({props:{align:{type:String,default:"right"},icon:{type:String,default:"dots"},options:{type:[Array,Function,String],default:()=>[]},text:{type:[Boolean,String],default:!0},theme:{type:String,default:"dark"}},computed:{hasSingleOption(){return Array.isArray(this.options)&&1===this.options.length}},methods:{onAction(t,e,s){"function"==typeof t?t.call(this):(this.$emit("action",t,e,s),this.$emit("option",t,e,s))},toggle(){this.$refs.options.toggle()}}},(function(){var t=this,e=t._self._c;return t.hasSingleOption?e("k-button",{staticClass:"k-options-dropdown-toggle",attrs:{icon:t.options[0].icon||t.icon,tooltip:t.options[0].tooltip||t.options[0].text},on:{click:function(e){return t.onAction(t.options[0].option||t.options[0].click,t.options[0],0)}}},[!0===t.text?[t._v(" "+t._s(t.options[0].text)+" ")]:!1!==t.text?[t._v(" "+t._s(t.text)+" ")]:t._e()],2):t.options.length?e("k-dropdown",{staticClass:"k-options-dropdown"},[e("k-button",{staticClass:"k-options-dropdown-toggle",attrs:{icon:t.icon,tooltip:t.$t("options")},on:{click:function(e){return t.$refs.options.toggle()}}},[t.text&&!0!==t.text?[t._v(" "+t._s(t.text)+" ")]:t._e()],2),e("k-dropdown-content",{ref:"options",staticClass:"k-options-dropdown-content",attrs:{align:t.align,options:t.options},on:{action:t.onAction}})],1):t._e()}),[],!1,null,null,null,null).exports;const $i=It({props:{align:{type:String,default:"left"},details:{type:Boolean,default:!1},dropdown:{type:Boolean,default:!0},keys:{type:Boolean,default:!1},limit:{type:Number,default:10},page:{type:Number,default:1},pageLabel:{type:String,default:()=>window.panel.$t("pagination.page")},total:{type:Number,default:0},prevLabel:{type:String,default:()=>window.panel.$t("prev")},nextLabel:{type:String,default:()=>window.panel.$t("next")},validate:{type:Function,default:()=>Promise.resolve()}},data(){return{currentPage:this.page}},computed:{show(){return this.pages>1},start(){return(this.currentPage-1)*this.limit+1},end(){let t=this.start-1+this.limit;return t>this.total?this.total:t},detailsText(){return 1===this.limit?this.start+" / ":this.start+"-"+this.end+" / "},pages(){return Math.ceil(this.total/this.limit)},hasPrev(){return this.start>1},hasNext(){return this.endthis.limit},offset(){return this.start-1}},watch:{page(t){this.currentPage=parseInt(t)}},created(){!0===this.keys&&window.addEventListener("keydown",this.navigate,!1)},destroyed(){window.removeEventListener("keydown",this.navigate,!1)},methods:{async goTo(t){try{await this.validate(t),t<1&&(t=1),t>this.pages&&(t=this.pages),this.currentPage=t,this.$refs.dropdown&&this.$refs.dropdown.close(),this.$emit("paginate",{page:this.currentPage,start:this.start,end:this.end,limit:this.limit,offset:this.offset})}catch(e){}},prev(){this.goTo(this.currentPage-1)},next(){this.goTo(this.currentPage+1)},navigate(t){switch(t.code){case"ArrowLeft":this.prev();break;case"ArrowRight":this.next()}}}},(function(){var t=this,e=t._self._c;return t.show?e("nav",{staticClass:"k-pagination",attrs:{"data-align":t.align}},[t.show?e("k-button",{attrs:{disabled:!t.hasPrev,tooltip:t.prevLabel,icon:"angle-left"},on:{click:t.prev}}):t._e(),t.details?[t.dropdown?[e("k-dropdown",[e("k-button",{staticClass:"k-pagination-details",attrs:{disabled:!t.hasPages},on:{click:function(e){return t.$refs.dropdown.toggle()}}},[t.total>1?[t._v(" "+t._s(t.detailsText)+" ")]:t._e(),t._v(" "+t._s(t.total)+" ")],2),e("k-dropdown-content",{ref:"dropdown",staticClass:"k-pagination-selector",on:{open:function(e){t.$nextTick((()=>t.$refs.page.focus()))}}},[e("div",{staticClass:"k-pagination-settings"},[e("label",{attrs:{for:"k-pagination-page"}},[e("span",[t._v(t._s(t.pageLabel)+":")]),e("select",{ref:"page",attrs:{id:"k-pagination-page"}},t._l(t.pages,(function(s){return e("option",{key:s,domProps:{selected:t.page===s,value:s}},[t._v(" "+t._s(s)+" ")])})),0)]),e("k-button",{attrs:{icon:"check"},on:{click:function(e){return t.goTo(t.$refs.page.value)}}})],1)])],1)]:[e("span",{staticClass:"k-pagination-details"},[t.total>1?[t._v(" "+t._s(t.detailsText)+" ")]:t._e(),t._v(" "+t._s(t.total)+" ")],2)]]:t._e(),t.show?e("k-button",{attrs:{disabled:!t.hasNext,tooltip:t.nextLabel,icon:"angle-right"},on:{click:t.next}}):t._e()],2):t._e()}),[],!1,null,null,null,null).exports;const _i=It({props:{prev:{type:[Boolean,Object],default:!1},next:{type:[Boolean,Object],default:!1}},computed:{buttons(){return[{...this.button(this.prev),icon:"angle-left"},{...this.button(this.next),icon:"angle-right"}]}},methods:{button:t=>t||{disabled:!0,link:"#"}}},(function(){return(0,this._self._c)("k-button-group",{staticClass:"k-prev-next",attrs:{buttons:this.buttons}})}),[],!1,null,null,null,null).exports;const xi=It({props:{types:{type:Object,default:()=>({})},type:String},data(){return{isLoading:!1,hasResults:!0,items:[],currentType:this.getType(this.type),q:null,selected:-1}},watch:{q(t,e){t!==e&&this.search(this.q)},currentType(t,e){t!==e&&this.search(this.q)},type(){this.currentType=this.getType(this.type)}},created(){this.search=dt(this.search,250),this.$events.$on("keydown.cmd.shift.f",this.open)},destroyed(){this.$events.$off("keydown.cmd.shift.f",this.open)},methods:{changeType(t){this.currentType=this.getType(t),this.$nextTick((()=>{this.$refs.input.focus()}))},close(){this.$refs.overlay.close(),this.hasResults=!0,this.items=[],this.q=null},getType(t){return this.types[t]||this.types[Object.keys(this.types)[0]]},navigate(t){this.$go(t.link),this.close()},onDown(){this.selected=0&&this.select(this.selected-1)},open(){this.$refs.overlay.open()},async search(t){this.isLoading=!0,this.$refs.types&&this.$refs.types.close();try{if(null===t||""===t)throw Error("Empty query");const e=await this.$search(this.currentType.id,t);if(!1===e)throw Error("JSON parsing failed");this.items=e.results}catch(e){this.items=[]}finally{this.select(-1),this.isLoading=!1,this.hasResults=this.items.length>0}},select(t){if(this.selected=t,this.$refs.items){const e=this.$refs.items.$el.querySelectorAll(".k-item");[...e].forEach((t=>delete t.dataset.selected)),t>=0&&(e[t].dataset.selected=!0)}}}},(function(){var t=this,e=t._self._c;return e("k-overlay",{ref:"overlay"},[e("div",{staticClass:"k-search",attrs:{role:"search"}},[e("div",{staticClass:"k-search-input"},[e("k-dropdown",{staticClass:"k-search-types"},[e("k-button",{attrs:{icon:t.currentType.icon,text:t.currentType.label},on:{click:function(e){return t.$refs.types.toggle()}}}),e("k-dropdown-content",{ref:"types"},t._l(t.types,(function(s,n){return e("k-dropdown-item",{key:n,attrs:{icon:s.icon},on:{click:function(e){return t.changeType(n)}}},[t._v(" "+t._s(s.label)+" ")])})),1)],1),e("input",{directives:[{name:"model",rawName:"v-model",value:t.q,expression:"q"}],ref:"input",attrs:{placeholder:t.$t("search")+" …","aria-label":t.$t("search"),autofocus:!0,type:"text"},domProps:{value:t.q},on:{input:[function(e){e.target.composing||(t.q=e.target.value)},function(e){t.hasResults=!0}],keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.preventDefault(),t.onDown.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.preventDefault(),t.onUp.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"tab",9,e.key,"Tab")?null:(e.preventDefault(),t.onTab.apply(null,arguments))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"enter",13,e.key,"Enter")?null:t.onEnter.apply(null,arguments)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:t.close.apply(null,arguments)}]}}),e("k-button",{staticClass:"k-search-close",attrs:{icon:t.isLoading?"loader":"cancel",tooltip:t.$t("close")},on:{click:t.close}})],1),!t.q||t.hasResults&&!t.items.length?t._e():e("div",{staticClass:"k-search-results"},[t.items.length?e("k-collection",{ref:"items",attrs:{items:t.items},on:{hover:t.onHover},nativeOn:{mouseout:function(e){return t.select(-1)}}}):t.hasResults?t._e():e("p",{staticClass:"k-search-empty"},[t._v(" "+t._s(t.$t("search.results.none"))+" ")])],1)])])}),[],!1,null,null,null,null).exports;const wi=It({props:{removable:Boolean},methods:{remove(){this.removable&&this.$emit("remove")},focus(){this.$refs.button.focus()}}},(function(){var t=this,e=t._self._c;return e("span",{ref:"button",staticClass:"k-tag",attrs:{tabindex:"0"},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"delete",[8,46],e.key,["Backspace","Delete","Del"])?null:(e.preventDefault(),t.remove.apply(null,arguments))}}},[e("span",{staticClass:"k-tag-text"},[t._t("default")],2),t.removable?e("k-icon",{staticClass:"k-tag-toggle",attrs:{type:"cancel-small"},nativeOn:{click:function(e){return t.remove.apply(null,arguments)}}}):t._e()],1)}),[],!1,null,null,null,null).exports;const Si=It({props:{breadcrumb:Array,license:Boolean,menu:Array,title:String,view:Object},computed:{notification(){return this.$store.state.notification.type&&"error"!==this.$store.state.notification.type?this.$store.state.notification:null}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-topbar"},[e("k-view",[e("div",{staticClass:"k-topbar-wrapper"},[e("k-dropdown",{staticClass:"k-topbar-menu"},[e("k-button",{staticClass:"k-topbar-button k-topbar-menu-button",attrs:{tooltip:t.$t("menu"),icon:"bars"},on:{click:function(e){return t.$refs.menu.toggle()}}},[e("k-icon",{attrs:{type:"angle-down"}})],1),e("k-dropdown-content",{ref:"menu",staticClass:"k-topbar-menu",attrs:{options:t.menu,theme:"light"}})],1),e("k-breadcrumb",{staticClass:"k-topbar-breadcrumb",attrs:{crumbs:t.breadcrumb,view:t.view}}),e("div",{staticClass:"k-topbar-signals"},[t.notification?e("k-button",{staticClass:"k-topbar-notification k-topbar-button",attrs:{text:t.notification.message,theme:"positive"},on:{click:function(e){return t.$store.dispatch("notification/close")}}}):t.license?t._e():e("k-registration"),e("k-form-indicator"),e("k-button",{staticClass:"k-topbar-button",attrs:{tooltip:t.$t("search"),icon:"search"},on:{click:function(e){return t.$refs.search.open()}}})],1)],1)]),e("k-search",{ref:"search",attrs:{type:t.$view.search||"pages",types:t.$searches}})],1)}),[],!1,null,null,null,null).exports;const Ci=It({props:{empty:String,blueprint:String,lock:[Boolean,Object],parent:String,tab:Object},computed:{content(){return this.$store.getters["content/values"]()}},methods:{exists(t){return this.$helper.isComponent(`k-${t}-section`)},meetsCondition(t){if(!t.when)return!0;let e=!0;return Object.keys(t.when).forEach((s=>{this.content[s.toLowerCase()]!==t.when[s]&&(e=!1)})),e}}},(function(){var t=this,e=t._self._c;return 0===t.tab.columns.length?e("k-box",{attrs:{html:!0,text:t.empty,theme:"info"}}):e("k-grid",{staticClass:"k-sections",attrs:{gutter:"large"}},t._l(t.tab.columns,(function(s,n){return e("k-column",{key:t.parent+"-column-"+n,attrs:{width:s.width,sticky:s.sticky}},[t._l(s.sections,(function(i,o){return[t.meetsCondition(i)?[t.exists(i.type)?e("k-"+i.type+"-section",t._b({key:t.parent+"-column-"+n+"-section-"+o+"-"+t.blueprint,tag:"component",class:"k-section k-section-name-"+i.name,attrs:{column:s.width,lock:t.lock,name:i.name,parent:t.parent,timestamp:t.$view.timestamp},on:{submit:function(e){return t.$emit("submit",e)}}},"component",i,!1)):[e("k-box",{key:t.parent+"-column-"+n+"-section-"+o,attrs:{text:t.$t("error.section.type.invalid",{type:i.type}),theme:"negative"}})]]:t._e()]}))],2)})),1)}),[],!1,null,null,null,null).exports;const Oi=It({mixins:[At],inheritAttrs:!1,data:()=>({fields:{},isLoading:!0,issue:null}),computed:{values(){return this.$store.getters["content/values"]()}},watch:{timestamp(){this.fetch()}},created(){this.input=dt(this.input,50),this.fetch()},methods:{input(t,e,s){this.$store.dispatch("content/update",[s,t[s]])},async fetch(){try{const t=await this.load();this.fields=t.fields,Object.keys(this.fields).forEach((t=>{this.fields[t].section=this.name,this.fields[t].endpoints={field:this.parent+"/fields/"+t,section:this.parent+"/sections/"+this.name,model:this.parent}}))}catch(t){this.issue=t}finally{this.isLoading=!1}},onSubmit(t){this.$events.$emit("keydown.cmd.s",t)}}},(function(){var t=this,e=t._self._c;return t.isLoading?t._e():e("section",{staticClass:"k-fields-section"},[t.issue?[e("k-headline",{staticClass:"k-fields-issue-headline"},[t._v(" Error ")]),e("k-box",{attrs:{text:t.issue.message,html:!1,theme:"negative"}})]:t._e(),e("k-form",{attrs:{fields:t.fields,validate:!0,value:t.values,disabled:t.lock&&"lock"===t.lock.state},on:{input:t.input,submit:t.onSubmit}})],2)}),[],!1,null,null,null,null).exports;const Ai=It({inheritAttrs:!1,props:{blueprint:String,column:String,parent:String,name:String,timestamp:Number},data:()=>({data:[],error:null,isLoading:!1,isProcessing:!1,options:{columns:{},empty:null,headline:null,help:null,layout:"list",link:null,max:null,min:null,size:null,sortable:null},pagination:{page:null},searchterm:null,searching:!1}),computed:{addIcon:()=>"add",buttons(){let t=[];return this.canSearch&&t.push({icon:"filter",text:this.$t("search"),click:this.onSearchToggle,responsive:!0}),this.canAdd&&t.push({icon:this.addIcon,text:this.$t("add"),click:this.onAdd}),t},canAdd:()=>!0,canDrop:()=>!1,canSearch(){return this.options.search},collection(){return{columns:this.options.columns,empty:this.emptyPropsWithSearch,layout:this.options.layout,help:this.options.help,items:this.items,pagination:this.pagination,sortable:!this.isProcessing&&this.options.sortable,size:this.options.size}},emptyProps(){return{icon:"page",text:this.$t("pages.empty")}},emptyPropsWithSearch(){return{...this.emptyProps,text:this.searching?this.$t("search.results.none"):this.options.empty||this.emptyProps.text}},items(){return this.data},isInvalid(){var t;return!((null==(t=this.searchterm)?void 0:t.length)>0)&&(!!(this.options.min&&this.data.lengththis.options.max))},paginationId(){return"kirby$pagination$"+this.parent+"/"+this.name},type:()=>"models"},watch:{searchterm:dt((function(){this.pagination.page=0,this.reload()}),200),timestamp(){this.reload()}},created(){this.load()},methods:{async load(t){t||(this.isLoading=!0),this.isProcessing=!0,null===this.pagination.page&&(this.pagination.page=localStorage.getItem(this.paginationId)||1);try{const t=await this.$api.get(this.parent+"/sections/"+this.name,{page:this.pagination.page,searchterm:this.searchterm});this.options=t.options,this.pagination=t.pagination,this.data=t.data}catch(e){this.error=e.message}finally{this.isProcessing=!1,this.isLoading=!1}},onAction(){},onAdd(){},onChange(){},onDrop(){},onSort(){},onPaginate(t){localStorage.setItem(this.paginationId,t.page),this.pagination=t,this.reload()},onSearchToggle(){this.searching=!this.searching,this.searchterm=null},onUpload(){},async reload(){await this.load(!0)},update(){this.reload(),this.$events.$emit("model.update")}}},(function(){var t=this,e=t._self._c;return!1===t.isLoading?e("section",{class:`k-models-section k-${t.type}-section`,attrs:{"data-processing":t.isProcessing}},[e("header",{staticClass:"k-section-header"},[e("k-headline",{attrs:{link:t.options.link}},[t._v(" "+t._s(t.options.headline||" ")+" "),t.options.min?e("abbr",{attrs:{title:t.$t("section.required")}},[t._v("*")]):t._e()]),e("k-button-group",{attrs:{buttons:t.buttons}})],1),t.error?e("k-box",{attrs:{theme:"negative"}},[e("k-text",{attrs:{size:"small"}},[e("strong",[t._v(" "+t._s(t.$t("error.section.notLoaded",{name:t.name}))+": ")]),t._v(" "+t._s(t.error)+" ")])],1):[e("k-dropzone",{attrs:{disabled:!t.canDrop},on:{drop:t.onDrop}},[t.searching&&t.options.search?e("k-input",{staticClass:"k-models-section-search",attrs:{autofocus:!0,placeholder:t.$t("search")+" …",type:"text"},on:{keydown:function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"esc",27,e.key,["Esc","Escape"])?null:t.onSearchToggle.apply(null,arguments)}},model:{value:t.searchterm,callback:function(e){t.searchterm=e},expression:"searchterm"}}):t._e(),e("k-collection",t._g(t._b({attrs:{"data-invalid":t.isInvalid},on:{action:t.onAction,change:t.onChange,sort:t.onSort,paginate:t.onPaginate}},"k-collection",t.collection,!1),t.canAdd?{empty:t.onAdd}:{}))],1),e("k-upload",{ref:"upload",on:{success:t.onUpload,error:t.reload}})]],2):t._e()}),[],!1,null,null,null,null).exports;const Ti=It({extends:Ai,computed:{addIcon:()=>"upload",canAdd(){return this.$permissions.files.create&&!1!==this.options.upload},canDrop(){return!1!==this.canAdd},emptyProps(){return{icon:"image",text:this.$t("files.empty")}},items(){return this.data.map((t=>(t.sortable=this.options.sortable,t.column=this.column,t.options=this.$dropdown(t.link,{query:{view:"list",update:this.options.sortable,delete:this.data.length>this.options.min}}),t.data={"data-id":t.id,"data-template":t.template},t)))},type:()=>"files",uploadProps(){return{...this.options.upload,url:this.$urls.api+"/"+this.options.upload.api}}},created(){this.load(),this.$events.$on("model.update",this.reload),this.$events.$on("file.sort",this.reload)},destroyed(){this.$events.$off("model.update",this.reload),this.$events.$off("file.sort",this.reload)},methods:{onAction(t,e){"replace"===t&&this.replace(e)},onAdd(){this.canAdd&&this.$refs.upload.open(this.uploadProps)},onDrop(t){this.canAdd&&this.$refs.upload.drop(t,this.uploadProps)},async onSort(t){if(!1===this.options.sortable)return!1;this.isProcessing=!0;try{await this.$api.patch(this.options.apiUrl+"/files/sort",{files:t.map((t=>t.id)),index:this.pagination.offset}),this.$store.dispatch("notification/success",":)"),this.$events.$emit("file.sort")}catch(e){this.reload(),this.$store.dispatch("notification/error",e.message)}finally{this.isProcessing=!1}},onUpload(){this.$events.$emit("file.create"),this.$events.$emit("model.update"),this.$store.dispatch("notification/success",":)")},replace(t){this.$refs.upload.open({url:this.$urls.api+"/"+t.link,accept:"."+t.extension+","+t.mime,multiple:!1})}}},null,null,!1,null,null,null,null).exports;const Ii=It({mixins:[At],data:()=>({headline:null,text:null,theme:null}),async created(){const t=await this.load();this.headline=t.headline,this.text=t.text,this.theme=t.theme||"info"}},(function(){var t=this,e=t._self._c;return e("section",{staticClass:"k-info-section"},[e("k-headline",{staticClass:"k-info-section-headline"},[t._v(" "+t._s(t.headline)+" ")]),e("k-box",{attrs:{theme:t.theme}},[e("k-text",{attrs:{html:t.text}})],1)],1)}),[],!1,null,null,null,null).exports;const Mi=It({extends:Ai,computed:{canAdd(){return this.options.add&&this.$permissions.pages.create},items(){return this.data.map((t=>{const e=!1!==t.permissions.changeStatus;return t.flag={status:t.status,tooltip:this.$t("page.status"),disabled:!e,click:()=>this.$dialog(t.link+"/changeStatus")},t.sortable=t.permissions.sort&&this.options.sortable,t.deletable=this.data.length>this.options.min,t.column=this.column,t.options=this.$dropdown(t.link,{query:{view:"list",delete:t.deletable,sort:t.sortable}}),t.data={"data-id":t.id,"data-status":t.status,"data-template":t.template},t}))}},created(){this.load(),this.$events.$on("page.changeStatus",this.reload),this.$events.$on("page.sort",this.reload)},destroyed(){this.$events.$off("page.changeStatus",this.reload),this.$events.$off("page.sort",this.reload)},methods:{onAdd(){this.canAdd&&this.$dialog("pages/create",{query:{parent:this.options.link||this.parent,view:this.parent,section:this.name}})},async onChange(t){let e=null;if(t.added&&(e="added"),t.moved&&(e="moved"),e){this.isProcessing=!0;const n=t[e].element,i=t[e].newIndex+1+this.pagination.offset;try{await this.$api.pages.changeStatus(n.id,"listed",i),this.$store.dispatch("notification/success",":)"),this.$events.$emit("page.sort",n)}catch(s){this.$store.dispatch("notification/error",{message:s.message,details:s.details}),await this.reload()}finally{this.isProcessing=!1}}}}},null,null,!1,null,null,null,null).exports;const Ei=It({mixins:[At],data:()=>({isLoading:!0,headline:null,reports:null,size:null}),async created(){const t=await this.load();this.isLoading=!1,this.headline=t.headline,this.reports=t.reports,this.size=t.size},methods:{}},(function(){var t=this,e=t._self._c;return!1===t.isLoading?e("section",{staticClass:"k-stats-section"},[e("header",{staticClass:"k-section-header"},[e("k-headline",[t._v(" "+t._s(t.headline)+" ")])],1),t.reports.length>0?e("k-stats",{attrs:{reports:t.reports,size:t.size}}):e("k-empty",{attrs:{icon:"chart"}},[t._v(" "+t._s(t.empty||t.$t("stats.empty")))])],1):t._e()}),[],!1,null,null,null,null).exports;t.component("k-sections",Ci),t.component("k-fields-section",Oi),t.component("k-files-section",Ti),t.component("k-info-section",Ii),t.component("k-pages-section",Mi),t.component("k-stats-section",Ei);const Li=It({props:{blueprint:String,next:Object,prev:Object,permissions:{type:Object,default:()=>({})},lock:{type:[Boolean,Object]},model:{type:Object,default:()=>({})},tab:{type:Object,default:()=>({columns:[]})},tabs:{type:Array,default:()=>[]}},computed:{id(){return this.model.link},isLocked(){var t;return"lock"===(null==(t=this.lock)?void 0:t.state)},protectedFields:()=>[]},watch:{"model.id":{handler(){this.content()},immediate:!0}},created(){this.$events.$on("model.reload",this.reload),this.$events.$on("keydown.left",this.toPrev),this.$events.$on("keydown.right",this.toNext)},destroyed(){this.$events.$off("model.reload",this.reload),this.$events.$off("keydown.left",this.toPrev),this.$events.$off("keydown.right",this.toNext)},methods:{content(){this.$store.dispatch("content/create",{id:this.id,api:this.id,content:this.model.content,ignore:this.protectedFields})},async reload(){await this.$reload(),this.content()},toPrev(t){this.prev&&"body"===t.target.localName&&this.$go(this.prev.link)},toNext(t){this.next&&"body"===t.target.localName&&this.$go(this.next.link)}}},null,null,!1,null,null,null,null).exports;const ji=It({extends:Li,computed:{avatarOptions(){return[{icon:"upload",text:this.$t("change"),click:()=>this.$refs.upload.open()},{icon:"trash",text:this.$t("delete"),click:this.deleteAvatar}]},buttons(){return[{icon:"email",text:`${this.$t("email")}: ${this.model.email}`,disabled:!this.permissions.changeEmail||this.isLocked,click:()=>this.$dialog(this.id+"/changeEmail")},{icon:"bolt",text:`${this.$t("role")}: ${this.model.role}`,disabled:!this.permissions.changeRole||this.isLocked,click:()=>this.$dialog(this.id+"/changeRole")},{icon:"globe",text:`${this.$t("language")}: ${this.model.language}`,disabled:!this.permissions.changeLanguage||this.isLocked,click:()=>this.$dialog(this.id+"/changeLanguage")}]},uploadApi(){return this.$urls.api+"/"+this.id+"/avatar"}},methods:{async deleteAvatar(){await this.$api.users.deleteAvatar(this.model.id),this.avatar=null,this.$store.dispatch("notification/success",":)"),this.$reload()},onAvatar(){this.model.avatar?this.$refs.picture.toggle():this.$refs.upload.open()},uploadedAvatar(){this.$store.dispatch("notification/success",":)"),this.$reload()}}},(function(){var t=this,e=t._self._c;return e("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[e("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[e("div",{staticClass:"k-user-view",attrs:{"data-locked":t.isLocked,"data-id":t.model.id,"data-template":t.blueprint}},[e("div",{staticClass:"k-user-profile"},[e("k-view",[e("k-dropdown",[e("k-button",{staticClass:"k-user-view-image",attrs:{tooltip:t.$t("avatar"),disabled:t.isLocked},on:{click:t.onAvatar}},[t.model.avatar?e("k-image",{attrs:{cover:!0,src:t.model.avatar,ratio:"1/1"}}):e("k-icon",{attrs:{back:"gray-900",color:"gray-200",type:"user"}})],1),t.model.avatar?e("k-dropdown-content",{ref:"picture",attrs:{options:t.avatarOptions}}):t._e()],1),e("k-button-group",{attrs:{buttons:t.buttons}})],1)],1),e("k-view",[e("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tab:t.tab.name,tabs:t.tabs},on:{edit:function(e){return t.$dialog(t.id+"/changeName")}},scopedSlots:t._u([{key:"left",fn:function(){return[e("k-button-group",[e("k-dropdown",{staticClass:"k-user-view-options"},[e("k-button",{attrs:{disabled:t.isLocked,text:t.$t("settings"),icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}}),e("k-dropdown-content",{ref:"settings",attrs:{options:t.$dropdown(t.id)}})],1),e("k-languages-dropdown")],1)]},proxy:!0},{key:"right",fn:function(){return[t.model.account?t._e():e("k-prev-next",{attrs:{prev:t.prev,next:t.next}})]},proxy:!0}])},[t.model.name&&0!==t.model.name.length?[t._v(" "+t._s(t.model.name)+" ")]:e("span",{staticClass:"k-user-name-placeholder"},[t._v(" "+t._s(t.$t("name"))+" … ")])],2),e("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("user.blueprint",{blueprint:t.$esc(t.blueprint)}),lock:t.lock,parent:t.id,tab:t.tab}}),e("k-upload",{ref:"upload",attrs:{url:t.uploadApi,multiple:!1,accept:"image/*"},on:{success:t.uploadedAvatar}})],1)],1)])}),[],!1,null,null,null,null).exports;const Di=It({extends:ji,prevnext:!1},null,null,!1,null,null,null,null).exports;const Bi=It({props:{error:String,layout:String}},(function(){var t=this,e=t._self._c;return e(`k-${t.layout}`,{tag:"component"},[e("k-view",{staticClass:"k-error-view"},[e("div",{staticClass:"k-error-view-content"},[e("k-text",[e("p",[e("k-icon",{staticClass:"k-error-view-icon",attrs:{type:"alert"}})],1),t._t("default",(function(){return[e("p",[t._v(" "+t._s(t.error)+" ")])]}))],2)],1)])],1)}),[],!1,null,null,null,null).exports;const Pi=It({extends:Li,props:{preview:Object},methods:{action(t){if("replace"===t)this.$refs.upload.open({url:this.$urls.api+"/"+this.id,accept:"."+this.model.extension+","+this.model.mime,multiple:!1})},onUpload(){this.$store.dispatch("notification/success",":)"),this.$reload()}}},(function(){var t=this,e=t._self._c;return e("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[e("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[e("div",{staticClass:"k-file-view",attrs:{"data-locked":t.isLocked,"data-id":t.model.id,"data-template":t.blueprint}},[e("k-file-preview",t._b({},"k-file-preview",t.preview,!1)),e("k-view",{staticClass:"k-file-content"},[e("k-header",{attrs:{editable:t.permissions.changeName&&!t.isLocked,tab:t.tab.name,tabs:t.tabs},on:{edit:function(e){return t.$dialog(t.id+"/changeName")}},scopedSlots:t._u([{key:"left",fn:function(){return[e("k-button-group",[e("k-button",{staticClass:"k-file-view-options",attrs:{link:t.preview.url,responsive:!0,text:t.$t("open"),icon:"open",target:"_blank"}}),e("k-dropdown",{staticClass:"k-file-view-options"},[e("k-button",{attrs:{disabled:t.isLocked,responsive:!0,text:t.$t("settings"),icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}}),e("k-dropdown-content",{ref:"settings",attrs:{options:t.$dropdown(t.id)},on:{action:t.action}})],1),e("k-languages-dropdown")],1)]},proxy:!0},{key:"right",fn:function(){return[e("k-prev-next",{attrs:{prev:t.prev,next:t.next}})]},proxy:!0}])},[t._v(" "+t._s(t.model.filename)+" ")]),e("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("file.blueprint",{blueprint:t.$esc(t.blueprint)}),lock:t.lock,parent:t.id,tab:t.tab}}),e("k-upload",{ref:"upload",on:{success:t.onUpload}})],1)],1)])}),[],!1,null,null,null,null).exports;const Ni=It({props:{isInstallable:Boolean,isInstalled:Boolean,isOk:Boolean,requirements:Object,translations:Array},data(){return{user:{name:"",email:"",language:this.$translation.code,password:"",role:"admin"}}},computed:{fields(){return{email:{label:this.$t("email"),type:"email",link:!1,autofocus:!0,required:!0},password:{label:this.$t("password"),type:"password",placeholder:this.$t("password")+" …",required:!0},language:{label:this.$t("language"),type:"select",options:this.translations,icon:"globe",empty:!1,required:!0}}},isReady(){return this.isOk&&this.isInstallable},isComplete(){return this.isOk&&this.isInstalled}},methods:{async install(){try{await this.$api.system.install(this.user),await this.$reload({globals:["$system","$translation"]}),this.$store.dispatch("notification/success",this.$t("welcome")+"!")}catch(t){this.$store.dispatch("notification/error",t)}}}},(function(){var t=this,e=t._self._c;return e("k-panel",[e("k-view",{staticClass:"k-installation-view",attrs:{align:"center"}},[t.isComplete?e("k-text",[e("k-headline",[t._v(t._s(t.$t("installation.completed")))]),e("k-link",{attrs:{to:"/login"}},[t._v(" "+t._s(t.$t("login"))+" ")])],1):t.isReady?e("form",{on:{submit:function(e){return e.preventDefault(),t.install.apply(null,arguments)}}},[e("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("installation"))+" ")]),e("k-fieldset",{attrs:{fields:t.fields,novalidate:!0},model:{value:t.user,callback:function(e){t.user=e},expression:"user"}}),e("k-button",{attrs:{text:t.$t("install"),type:"submit",icon:"check"}})],1):e("div",[e("k-headline",[t._v(" "+t._s(t.$t("installation.issues.headline"))+" ")]),e("ul",{staticClass:"k-installation-issues"},[!1===t.isInstallable?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.disabled"))}})],1):t._e(),!1===t.requirements.php?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.php"))}})],1):t._e(),!1===t.requirements.server?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.server"))}})],1):t._e(),!1===t.requirements.mbstring?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.mbstring"))}})],1):t._e(),!1===t.requirements.curl?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.curl"))}})],1):t._e(),!1===t.requirements.accounts?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.accounts"))}})],1):t._e(),!1===t.requirements.content?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.content"))}})],1):t._e(),!1===t.requirements.media?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.media"))}})],1):t._e(),!1===t.requirements.sessions?e("li",[e("k-icon",{attrs:{type:"alert"}}),e("span",{domProps:{innerHTML:t._s(t.$t("installation.issues.sessions"))}})],1):t._e()]),e("k-button",{attrs:{text:t.$t("retry"),icon:"refresh"},on:{click:t.$reload}})],1)],1)],1)}),[],!1,null,null,null,null).exports;const qi=It({props:{languages:{type:Array,default:()=>[]}},computed:{languagesCollection(){return this.languages.map((t=>({...t,image:{back:"black",color:"gray",icon:"globe"},link:()=>{this.$dialog(`languages/${t.id}/update`)},options:[{icon:"edit",text:this.$t("edit"),click(){this.$dialog(`languages/${t.id}/update`)}},{icon:"trash",text:this.$t("delete"),disabled:t.default&&1!==this.languages.length,click(){this.$dialog(`languages/${t.id}/delete`)}}]})))},primaryLanguage(){return this.languagesCollection.filter((t=>t.default))},secondaryLanguages(){return this.languagesCollection.filter((t=>!1===t.default))}}},(function(){var t=this,e=t._self._c;return e("k-inside",[e("k-view",{staticClass:"k-languages-view"},[e("k-header",[t._v(" "+t._s(t.$t("view.languages"))+" "),e("k-button-group",{attrs:{slot:"left"},slot:"left"},[e("k-button",{attrs:{text:t.$t("language.create"),icon:"add"},on:{click:function(e){return t.$dialog("languages/create")}}})],1)],1),e("section",{staticClass:"k-languages"},[t.languages.length>0?[e("section",{staticClass:"k-languages-view-section"},[e("header",{staticClass:"k-languages-view-section-header"},[e("k-headline",[t._v(t._s(t.$t("languages.default")))])],1),e("k-collection",{attrs:{items:t.primaryLanguage}})],1),e("section",{staticClass:"k-languages-view-section"},[e("header",{staticClass:"k-languages-view-section-header"},[e("k-headline",[t._v(t._s(t.$t("languages.secondary")))])],1),t.secondaryLanguages.length?e("k-collection",{attrs:{items:t.secondaryLanguages}}):e("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$dialog("languages/create")}}},[t._v(" "+t._s(t.$t("languages.secondary.empty"))+" ")])],1)]:0===t.languages.length?[e("k-empty",{attrs:{icon:"globe"},on:{click:function(e){return t.$dialog("languages/create")}}},[t._v(" "+t._s(t.$t("languages.empty"))+" ")])]:t._e()],2)],1)],1)}),[],!1,null,null,null,null).exports;const Fi=It({components:{"k-login-plugin":window.panel.plugins.login||pe},props:{methods:Array,pending:Object},computed:{form(){return this.pending.email?"code":this.$user?null:"login"}},created(){this.$store.dispatch("content/clear")}},(function(){var t=this,e=t._self._c;return e("k-panel",["login"===t.form?e("k-view",{staticClass:"k-login-view",attrs:{align:"center"}},[e("k-login-plugin",{attrs:{methods:t.methods}})],1):"code"===t.form?e("k-view",{staticClass:"k-login-code-view",attrs:{align:"center"}},[e("k-login-code",t._b({},"k-login-code",t.$props,!1))],1):t._e()],1)}),[],!1,null,null,null,null).exports;const Ri=It({extends:Li,props:{status:Object},computed:{protectedFields:()=>["title"]}},(function(){var t=this,e=t._self._c;return e("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[e("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[e("k-view",{staticClass:"k-page-view",attrs:{"data-locked":t.isLocked,"data-id":t.model.id,"data-template":t.blueprint}},[e("k-header",{attrs:{editable:t.permissions.changeTitle&&!t.isLocked,tab:t.tab.name,tabs:t.tabs},on:{edit:function(e){return t.$dialog(t.id+"/changeTitle")}},scopedSlots:t._u([{key:"left",fn:function(){return[e("k-button-group",[t.permissions.preview&&t.model.previewUrl?e("k-button",{staticClass:"k-page-view-preview",attrs:{link:t.model.previewUrl,responsive:!0,text:t.$t("open"),icon:"open",target:"_blank"}}):t._e(),t.status?e("k-status-icon",{attrs:{status:t.model.status,disabled:!t.permissions.changeStatus||t.isLocked,responsive:!0,text:t.status.label},on:{click:function(e){return t.$dialog(t.id+"/changeStatus")}}}):t._e(),e("k-dropdown",{staticClass:"k-page-view-options"},[e("k-button",{attrs:{disabled:!0===t.isLocked,responsive:!0,text:t.$t("settings"),icon:"cog"},on:{click:function(e){return t.$refs.settings.toggle()}}}),e("k-dropdown-content",{ref:"settings",attrs:{options:t.$dropdown(t.id)}})],1),e("k-languages-dropdown")],1)]},proxy:!0},{key:"right",fn:function(){return[t.model.id?e("k-prev-next",{attrs:{prev:t.prev,next:t.next}}):t._e()]},proxy:!0}])},[t._v(" "+t._s(t.model.title)+" ")]),e("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("page.blueprint",{blueprint:t.$esc(t.blueprint)}),lock:t.lock,parent:t.id,tab:t.tab}})],1)],1)}),[],!1,null,null,null,null).exports;const zi=It({props:{id:String},computed:{view(){return"k-"+this.id+"-plugin-view"}}},(function(){var t=this._self._c;return t("k-inside",[t(this.view,{tag:"component"})],1)}),[],!1,null,null,null,null).exports;const Yi=It({data:()=>({isLoading:!1,issue:"",values:{password:null,passwordConfirmation:null}}),computed:{fields(){return{password:{autofocus:!0,label:this.$t("user.changePassword.new"),icon:"key",type:"password"},passwordConfirmation:{label:this.$t("user.changePassword.new.confirm"),icon:"key",type:"password"}}}},mounted(){this.$store.dispatch("title",this.$t("view.resetPassword"))},methods:{async submit(){if(!this.values.password||this.values.password.length<8)return this.issue=this.$t("error.user.password.invalid"),!1;if(this.values.password!==this.values.passwordConfirmation)return this.issue=this.$t("error.user.password.notSame"),!1;this.isLoading=!0;try{await this.$api.users.changePassword(this.$user.id,this.values.password),this.$store.dispatch("notification/success",":)"),this.$go("/")}catch(t){this.issue=t.message}finally{this.isLoading=!1}}}},(function(){var t=this,e=t._self._c;return e("k-inside",[e("k-view",{staticClass:"k-password-reset-view",attrs:{align:"center"}},[e("k-form",{attrs:{fields:t.fields,"submit-button":t.$t("change")},on:{submit:t.submit},scopedSlots:t._u([{key:"header",fn:function(){return[e("h1",{staticClass:"sr-only"},[t._v(" "+t._s(t.$t("view.resetPassword"))+" ")]),t.issue?e("k-login-alert",{on:{click:function(e){t.issue=null}}},[t._v(" "+t._s(t.issue)+" ")]):t._e(),e("k-user-info",{attrs:{user:t.$user}})]},proxy:!0},{key:"footer",fn:function(){return[e("div",{staticClass:"k-login-buttons"},[e("k-button",{staticClass:"k-login-button",attrs:{icon:"check",type:"submit"}},[t._v(" "+t._s(t.$t("change"))+" "),t.isLoading?[t._v(" … ")]:t._e()],2)],1)]},proxy:!0}]),model:{value:t.values,callback:function(e){t.values=e},expression:"values"}})],1)],1)}),[],!1,null,null,null,null).exports;const Hi=It({extends:Li,computed:{protectedFields:()=>["title"]}},(function(){var t=this,e=t._self._c;return e("k-inside",{scopedSlots:t._u([{key:"footer",fn:function(){return[e("k-form-buttons",{attrs:{lock:t.lock}})]},proxy:!0}])},[e("k-view",{staticClass:"k-site-view",attrs:{"data-locked":t.isLocked,"data-id":"/","data-template":"site"}},[e("k-header",{attrs:{editable:t.permissions.changeTitle&&!t.isLocked,tabs:t.tabs,tab:t.tab.name},on:{edit:function(e){return t.$dialog("site/changeTitle")}},scopedSlots:t._u([{key:"left",fn:function(){return[e("k-button-group",[e("k-button",{staticClass:"k-site-view-preview",attrs:{link:t.model.previewUrl,responsive:!0,text:t.$t("open"),icon:"open",target:"_blank"}}),e("k-languages-dropdown")],1)]},proxy:!0}])},[t._v(" "+t._s(t.model.title)+" ")]),e("k-sections",{attrs:{blueprint:t.blueprint,empty:t.$t("site.blueprint"),lock:t.lock,tab:t.tab,parent:"site"},on:{submit:function(e){return t.$emit("submit",e)}}})],1)],1)}),[],!1,null,null,null,null).exports;const Ui=It({props:{debug:Boolean,license:String,php:String,plugins:Array,server:String,https:Boolean,urls:Object,version:String},data:()=>({security:[]}),computed:{environment(){return[{label:this.$t("license"),value:this.license?"Kirby 3":this.$t("license.unregistered.label"),theme:this.license?null:"negative",click:this.license?()=>this.$dialog("license"):()=>this.$dialog("registration")},{label:this.$t("version"),value:this.version,link:"https://github.com/getkirby/kirby/releases/tag/"+this.version},{label:"PHP",value:this.php},{label:this.$t("server"),value:this.server||"?"}]}},async created(){console.info("Running system health checks for the Panel system view; failed requests in the following console output are expected behavior.");let t=(Promise.allSettled||Promise.all).bind(Promise);await t([this.check("content"),this.check("debug"),this.check("git"),this.check("https"),this.check("kirby"),this.check("site")]),console.info("System health checks ended.")},methods:{async check(t){switch(t){case"debug":!0===this.debug&&this.securityIssue(t);break;case"https":!0!==this.https&&this.securityIssue(t);break;default:{const e=this.urls[t];if(!e)return!1;!0===await this.isAccessible(e)&&this.securityIssue(t)}}},securityIssue(t){this.security.push({image:{back:"var(--color-red-200)",icon:"alert",color:"var(--color-red)"},id:t,text:this.$t("system.issues."+t),link:"https://getkirby.com/security/"+t})},isAccessible:async t=>(await fetch(t,{cache:"no-store"})).status<400,retry(){this.$go(window.location.href)}}},(function(){var t=this,e=t._self._c;return e("k-inside",[e("k-view",{staticClass:"k-system-view"},[e("k-header",[t._v(" "+t._s(t.$t("view.system"))+" ")]),e("section",{staticClass:"k-system-view-section"},[e("header",{staticClass:"k-system-view-section-header"},[e("k-headline",[t._v(t._s(t.$t("environment")))])],1),e("k-stats",{staticClass:"k-system-info",attrs:{reports:t.environment,size:"medium"}})],1),t.security.length?e("section",{staticClass:"k-system-view-section"},[e("header",{staticClass:"k-system-view-section-header"},[e("k-headline",[t._v(t._s(t.$t("security")))]),e("k-button",{attrs:{tooltip:t.$t("retry"),icon:"refresh"},on:{click:t.retry}})],1),e("k-items",{attrs:{items:t.security}})],1):t._e(),t.plugins.length?e("section",{staticClass:"k-system-view-section"},[e("header",{staticClass:"k-system-view-section-header"},[e("k-headline",{attrs:{link:"https://getkirby.com/plugins"}},[t._v(" "+t._s(t.$t("plugins"))+" ")])],1),e("k-table",{attrs:{index:!1,columns:{name:{label:t.$t("name"),type:"url",mobile:!0},author:{label:t.$t("author")},license:{label:t.$t("license")},version:{label:t.$t("version"),width:"8rem",mobile:!0}},rows:t.plugins}})],1):t._e()],1)],1)}),[],!1,null,null,null,null).exports;const Ki=It({props:{role:Object,roles:Array,search:String,title:String,users:Object},computed:{items(){return this.users.data.map((t=>(t.options=this.$dropdown(t.link),t)))}},methods:{paginate(t){this.$reload({query:{page:t.page}})}}},(function(){var t=this,e=t._self._c;return e("k-inside",[e("k-view",{staticClass:"k-users-view"},[e("k-header",{scopedSlots:t._u([{key:"left",fn:function(){return[e("k-button-group",{attrs:{buttons:[{disabled:!1===t.$permissions.users.create,text:t.$t("user.create"),icon:"add",click:()=>t.$dialog("users/create")}]}})]},proxy:!0},{key:"right",fn:function(){return[e("k-button-group",[e("k-dropdown",[e("k-button",{attrs:{responsive:!0,text:`${t.$t("role")}: ${t.role?t.role.title:t.$t("role.all")}`,icon:"funnel"},on:{click:function(e){return t.$refs.roles.toggle()}}}),e("k-dropdown-content",{ref:"roles",attrs:{align:"right"}},[e("k-dropdown-item",{attrs:{icon:"bolt",link:"/users"}},[t._v(" "+t._s(t.$t("role.all"))+" ")]),e("hr"),t._l(t.roles,(function(s){return e("k-dropdown-item",{key:s.id,attrs:{link:"/users/?role="+s.id,icon:"bolt"}},[t._v(" "+t._s(s.title)+" ")])}))],2)],1)],1)]},proxy:!0}])},[t._v(" "+t._s(t.$t("view.users"))+" ")]),t.users.data.length>0?[e("k-collection",{attrs:{items:t.items,pagination:t.users.pagination},on:{paginate:t.paginate}})]:0===t.users.pagination.total?[e("k-empty",{attrs:{icon:"users"}},[t._v(" "+t._s(t.$t("role.empty"))+" ")])]:t._e()],2)],1)}),[],!1,null,null,null,null).exports;const Gi=It({computed:{placeholder(){return this.field("code",{}).placeholder},languages(){return this.field("language",{options:[]}).options}},methods:{focus(){this.$refs.code.focus()}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-block-type-code-editor"},[e("k-input",{ref:"code",attrs:{buttons:!1,placeholder:t.placeholder,spellcheck:!1,value:t.content.code,type:"textarea"},on:{input:function(e){return t.update({code:e})}}}),t.languages.length?e("div",{staticClass:"k-block-type-code-editor-language"},[e("k-icon",{attrs:{type:"code"}}),e("k-input",{ref:"language",attrs:{empty:!1,options:t.languages,value:t.content.language,type:"select"},on:{input:function(e){return t.update({language:e})}}})],1):t._e()],1)}),[],!1,null,null,null,null).exports,Ji=Object.freeze(Object.defineProperty({__proto__:null,default:Gi},Symbol.toStringTag,{value:"Module"}));const Vi=It({},(function(){var t=this;return(0,t._self._c)("k-block-title",{attrs:{content:t.content,fieldset:t.fieldset},on:{dblclick:function(e){return t.$emit("open")}}})}),[],!1,null,null,null,null).exports,Wi=Object.freeze(Object.defineProperty({__proto__:null,default:Vi},Symbol.toStringTag,{value:"Module"}));const Xi=It({},(function(){var t=this,e=t._self._c;return e("ul",{on:{dblclick:t.open}},[0===t.content.images.length?[e("li"),e("li"),e("li"),e("li"),e("li")]:t._l(t.content.images,(function(t){return e("li",{key:t.id},[e("img",{attrs:{src:t.url,srcset:t.image.srcset,alt:t.alt}})])}))],2)}),[],!1,null,null,null,null).exports,Zi=Object.freeze(Object.defineProperty({__proto__:null,default:Xi},Symbol.toStringTag,{value:"Module"}));const Qi=It({computed:{textField(){return this.field("text",{marks:!0})}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-block-type-heading-input",attrs:{"data-level":t.content.level}},[e("k-writer",{ref:"input",attrs:{inline:!0,marks:t.textField.marks,placeholder:t.textField.placeholder,value:t.content.text},on:{input:function(e){return t.update({text:e})}}})],1)}),[],!1,null,null,null,null).exports,to=Object.freeze(Object.defineProperty({__proto__:null,default:Qi},Symbol.toStringTag,{value:"Module"}));const eo=It({computed:{captionMarks(){return this.field("caption",{marks:!0}).marks},crop(){return this.content.crop||!1},src(){var t;return"web"===this.content.location?this.content.src:!!(null==(t=this.content.image[0])?void 0:t.url)&&this.content.image[0].url},ratio(){return this.content.ratio||!1}}},(function(){var t=this,e=t._self._c;return e("k-block-figure",{attrs:{caption:t.content.caption,"caption-marks":t.captionMarks,"empty-text":t.$t("field.blocks.image.placeholder")+" …","is-empty":!t.src,"empty-icon":"image"},on:{open:t.open,update:t.update}},[t.src?[t.ratio?e("k-aspect-ratio",{attrs:{ratio:t.ratio,cover:t.crop}},[e("img",{attrs:{alt:t.content.alt,src:t.src}})]):e("img",{staticClass:"k-block-type-image-auto",attrs:{alt:t.content.alt,src:t.src}})]:t._e()],2)}),[],!1,null,null,null,null).exports,so=Object.freeze(Object.defineProperty({__proto__:null,default:eo},Symbol.toStringTag,{value:"Module"}));const no=It({},(function(){return this._self._c,this._m(0)}),[function(){var t=this._self._c;return t("div",[t("hr")])}],!1,null,null,null,null).exports,io=Object.freeze(Object.defineProperty({__proto__:null,default:no},Symbol.toStringTag,{value:"Module"}));const oo=It({computed:{marks(){return this.field("text",{}).marks}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this;return(0,t._self._c)("k-input",{ref:"input",staticClass:"k-block-type-list-input",attrs:{marks:t.marks,value:t.content.text,type:"list"},on:{input:function(e){return t.update({text:e})}}})}),[],!1,null,null,null,null).exports,ro=Object.freeze(Object.defineProperty({__proto__:null,default:oo},Symbol.toStringTag,{value:"Module"}));const lo=It({computed:{placeholder(){return this.field("text",{}).placeholder}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this;return(0,t._self._c)("k-input",{ref:"input",staticClass:"k-block-type-markdown-input",attrs:{buttons:!1,placeholder:t.placeholder,spellcheck:!1,value:t.content.text,type:"textarea"},on:{input:function(e){return t.update({text:e})}}})}),[],!1,null,null,null,null).exports,ao=Object.freeze(Object.defineProperty({__proto__:null,default:lo},Symbol.toStringTag,{value:"Module"}));const uo=It({computed:{citationField(){return this.field("citation",{})},textField(){return this.field("text",{})}},methods:{focus(){this.$refs.text.focus()}}},(function(){var t,e,s=this,n=s._self._c;return n("div",{staticClass:"k-block-type-quote-editor"},[n("k-writer",{ref:"text",staticClass:"k-block-type-quote-text",attrs:{inline:null!=(t=s.textField.inline)&&t,marks:s.textField.marks,placeholder:s.textField.placeholder,value:s.content.text},on:{input:function(t){return s.update({text:t})}}}),n("k-writer",{ref:"citation",staticClass:"k-block-type-quote-citation",attrs:{inline:null==(e=s.citationField.inline)||e,marks:s.citationField.marks,placeholder:s.citationField.placeholder,value:s.content.citation},on:{input:function(t){return s.update({citation:t})}}})],1)}),[],!1,null,null,null,null).exports,co=Object.freeze(Object.defineProperty({__proto__:null,default:uo},Symbol.toStringTag,{value:"Module"}));const po=It({inheritAttrs:!1,computed:{columns(){return this.table.columns||this.fields},fields(){return this.table.fields||{}},rows(){return this.content.rows||[]},table(){let t=null;for(const e of Object.values(this.fieldset.tabs))e.fields.rows&&(t=e.fields.rows);return t||{}}}},(function(){var t=this;return(0,t._self._c)("k-table",{staticClass:"k-block-type-table-preview",attrs:{columns:t.columns,empty:t.$t("field.structure.empty"),rows:t.rows},nativeOn:{dblclick:function(e){return t.open.apply(null,arguments)}}})}),[],!1,null,null,null,null).exports,ho=Object.freeze(Object.defineProperty({__proto__:null,default:po},Symbol.toStringTag,{value:"Module"}));const mo=It({computed:{component(){const t="k-"+this.textField.type+"-input";return this.$helper.isComponent(t)?t:"k-writer"},textField(){return this.field("text",{})}},methods:{focus(){this.$refs.input.focus()}}},(function(){var t=this;return(0,t._self._c)(t.component,t._b({ref:"input",tag:"component",staticClass:"k-block-type-text-input",attrs:{value:t.content.text},on:{input:function(e){return t.update({text:e})}}},"component",t.textField,!1))}),[],!1,null,null,null,null).exports,fo=Object.freeze(Object.defineProperty({__proto__:null,default:mo},Symbol.toStringTag,{value:"Module"}));const go=It({computed:{captionMarks(){return this.field("caption",{marks:!0}).marks},video(){return this.$helper.embed.video(this.content.url,!0)}}},(function(){var t=this,e=t._self._c;return e("k-block-figure",{attrs:{caption:t.content.caption,"caption-marks":t.captionMarks,"empty-text":t.$t("field.blocks.video.placeholder")+" …","is-empty":!t.video,"empty-icon":"video"},on:{open:t.open,update:t.update}},[e("k-aspect-ratio",{attrs:{ratio:"16/9"}},[t.video?e("iframe",{attrs:{src:t.video,referrerpolicy:"strict-origin-when-cross-origin"}}):t._e()])],1)}),[],!1,null,null,null,null).exports,ko=Object.freeze(Object.defineProperty({__proto__:null,default:go},Symbol.toStringTag,{value:"Module"}));const bo=It({inheritAttrs:!1,props:{attrs:[Array,Object],content:[Array,Object],endpoints:Object,fieldset:Object,id:String,isBatched:Boolean,isFull:Boolean,isHidden:Boolean,isLastInBatch:Boolean,isSelected:Boolean,name:String,next:Object,prev:Object,type:String},data:()=>({skipFocus:!1}),computed:{className(){let t=["k-block-type-"+this.type];return this.fieldset.preview!==this.type&&t.push("k-block-type-"+this.fieldset.preview),!1===this.wysiwyg&&t.push("k-block-type-default"),t},customComponent(){return this.wysiwyg?this.wysiwygComponent:"k-block-type-default"},isEditable(){return!1!==this.fieldset.editable},listeners(){return{...this.$listeners,confirmToRemove:this.confirmToRemove,open:this.open}},tabs(){let t=this.fieldset.tabs;return Object.entries(t).forEach((([e,s])=>{Object.entries(s.fields).forEach((([s])=>{t[e].fields[s].section=this.name,t[e].fields[s].endpoints={field:this.endpoints.field+"/fieldsets/"+this.type+"/fields/"+s,section:this.endpoints.section,model:this.endpoints.model}}))})),t},wysiwyg(){return!1!==this.wysiwygComponent},wysiwygComponent(){if(!1===this.fieldset.preview)return!1;let t;return this.fieldset.preview&&(t="k-block-type-"+this.fieldset.preview,this.$helper.isComponent(t))?t:(t="k-block-type-"+this.type,!!this.$helper.isComponent(t)&&t)}},methods:{close(){this.$refs.drawer.close()},confirmToRemove(){this.$refs.removeDialog.open()},focus(){!0!==this.skipFocus&&("function"==typeof this.$refs.editor.focus?this.$refs.editor.focus():this.$refs.container.focus())},onFocusIn(t){var e,s;(null==(s=null==(e=this.$refs.options)?void 0:e.$el)?void 0:s.contains(t.target))||this.$emit("focus",t)},goTo(t){t&&(this.skipFocus=!0,this.close(),this.$nextTick((()=>{t.$refs.container.focus(),t.open(),this.skipFocus=!1})))},open(){var t;null==(t=this.$refs.drawer)||t.open()},remove(){this.$refs.removeDialog.close(),this.$emit("remove",this.id)}}},(function(){var t=this,e=t._self._c;return e("div",{ref:"container",staticClass:"k-block-container",class:"k-block-container-type-"+t.type,attrs:{"data-batched":t.isBatched,"data-disabled":t.fieldset.disabled,"data-hidden":t.isHidden,"data-id":t.id,"data-last-in-batch":t.isLastInBatch,"data-selected":t.isSelected,"data-translate":t.fieldset.translate,tabindex:"0"},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:e.ctrlKey&&e.shiftKey?(e.preventDefault(),t.$emit("sortDown")):null},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:e.ctrlKey&&e.shiftKey?(e.preventDefault(),t.$emit("sortUp")):null}],focus:function(e){return t.$emit("focus")},focusin:t.onFocusIn}},[e("div",{staticClass:"k-block",class:t.className},[e(t.customComponent,t._g(t._b({ref:"editor",tag:"component"},"component",t.$props,!1),t.listeners))],1),e("k-block-options",t._g({ref:"options",attrs:{"is-batched":t.isBatched,"is-editable":t.isEditable,"is-full":t.isFull,"is-hidden":t.isHidden}},t.listeners)),t.isEditable&&!t.isBatched?e("k-form-drawer",{ref:"drawer",staticClass:"k-block-drawer",attrs:{id:t.id,icon:t.fieldset.icon||"box",tabs:t.tabs,title:t.fieldset.name,value:t.content},on:{close:function(e){return t.focus()},input:function(e){return t.$emit("update",e)}},scopedSlots:t._u([{key:"options",fn:function(){return[t.isHidden?e("k-button",{staticClass:"k-drawer-option",attrs:{icon:"hidden"},on:{click:function(e){return t.$emit("show")}}}):t._e(),e("k-button",{staticClass:"k-drawer-option",attrs:{disabled:!t.prev,icon:"angle-left"},on:{click:function(e){return e.preventDefault(),e.stopPropagation(),t.goTo(t.prev)}}}),e("k-button",{staticClass:"k-drawer-option",attrs:{disabled:!t.next,icon:"angle-right"},on:{click:function(e){return e.preventDefault(),e.stopPropagation(),t.goTo(t.next)}}}),e("k-button",{staticClass:"k-drawer-option",attrs:{icon:"trash"},on:{click:function(e){return e.preventDefault(),e.stopPropagation(),t.confirmToRemove.apply(null,arguments)}}})]},proxy:!0}],null,!1,2211169536)}):t._e(),e("k-remove-dialog",{ref:"removeDialog",attrs:{text:t.$t("field.blocks.delete.confirm")},on:{submit:t.remove}})],1)}),[],!1,null,null,null,null).exports;const yo=It({components:{"k-block-pasteboard":It({inheritAttrs:!1,computed:{shortcut(){return this.$helper.keyboard.metaKey()+"+v"}},methods:{close(){this.$refs.dialog.close()},open(){this.$refs.dialog.open()},onPaste(t){this.$emit("paste",t),this.close()}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",staticClass:"k-block-importer",attrs:{"cancel-button":!1,"submit-button":!1,size:"large"}},[e("label",{attrs:{for:"pasteboard"},domProps:{innerHTML:t._s(t.$t("field.blocks.fieldsets.paste",{shortcut:t.shortcut}))}}),e("textarea",{attrs:{id:"pasteboard"},on:{paste:function(e){return e.preventDefault(),t.onPaste.apply(null,arguments)}}})])}),[],!1,null,null,null,null).exports},inheritAttrs:!1,props:{autofocus:Boolean,empty:String,endpoints:Object,fieldsets:Object,fieldsetGroups:Object,group:String,max:{type:Number,default:null},value:{type:Array,default:()=>[]}},data(){return{isMultiSelectKey:!1,batch:[],blocks:this.value,current:null,isFocussed:!1}},computed:{draggableOptions(){return{id:this._uid,handle:".k-sort-handle",list:this.blocks,move:this.move,delay:10,data:{fieldsets:this.fieldsets,isFull:this.isFull},options:{group:this.group}}},hasFieldsets(){return Object.keys(this.fieldsets).length},isEditing(){return this.$store.state.dialog||this.$store.state.drawers.open.length>0},isEmpty(){return 0===this.blocks.length},isFull(){return null!==this.max&&this.blocks.length>=this.max},selected(){return this.current},selectedOrBatched(){return this.batch.length>0?this.batch:this.selected?[this.selected]:[]}},watch:{value(){this.blocks=this.value}},created(){this.$events.$on("blur",this.onBlur),this.$events.$on("copy",this.copy),this.$events.$on("focus",this.onOutsideFocus),this.$events.$on("keydown",this.onKey),this.$events.$on("keyup",this.onKey),this.$events.$on("paste",this.onPaste)},destroyed(){this.$events.$off("blur",this.onBlur),this.$events.$off("copy",this.copy),this.$events.$off("focus",this.onOutsideFocus),this.$events.$off("keydown",this.onKey),this.$events.$off("keyup",this.onKey),this.$events.$off("paste",this.onPaste)},mounted(){!0===this.$props.autofocus&&this.focus()},methods:{append(t,e){if("string"!=typeof t){if(Array.isArray(t)){let s=this.$helper.clone(t).map((t=>(t.id=this.$helper.uuid(),t)));const n=Object.keys(this.fieldsets);if(s=s.filter((t=>n.includes(t.type))),this.max){const t=this.max-this.blocks.length;s=s.slice(0,t)}this.blocks.splice(e,0,...s),this.save()}}else this.add(t,e)},async add(t="text",e){const s=await this.$api.get(this.endpoints.field+"/fieldsets/"+t);this.blocks.splice(e,0,s),this.save(),this.$nextTick((()=>{this.focusOrOpen(s)}))},addToBatch(t){null!==this.selected&&!1===this.batch.includes(this.selected)&&(this.batch.push(this.selected),this.current=null),!1===this.batch.includes(t.id)&&this.batch.push(t.id)},choose(t){if(1===Object.keys(this.fieldsets).length){const e=Object.values(this.fieldsets)[0].type;this.add(e,t)}else this.$refs.selector.open(t)},chooseToConvert(t){this.$refs.selector.open(t,{disabled:[t.type],headline:this.$t("field.blocks.changeType"),event:"convert"})},click(t){this.$emit("click",t)},confirmToRemoveAll(){this.$refs.removeAll.open()},confirmToRemoveSelected(){this.$refs.removeSelected.open()},copy(t){if(!0===this.isEditing)return!1;if(0===this.blocks.length)return!1;if(0===this.selectedOrBatched.length)return!1;if(!0===this.isInputEvent(t))return!1;let e=[];if(this.blocks.forEach((t=>{this.selectedOrBatched.includes(t.id)&&e.push(t)})),0===e.length)return!1;this.$helper.clipboard.write(e,t),t instanceof ClipboardEvent==!1&&(this.batch=this.selectedOrBatched),this.$store.dispatch("notification/success",`${e.length} copied!`)},copyAll(){this.selectAll(),this.copy(),this.deselectAll()},async convert(t,e){var s;const n=this.findIndex(e.id);if(-1===n)return!1;const i=t=>{var e;let s={};for(const n of Object.values(null!=(e=null==t?void 0:t.tabs)?e:{}))s={...s,...n.fields};return s},o=this.blocks[n],r=await this.$api.get(this.endpoints.field+"/fieldsets/"+t),l=this.fieldsets[o.type],a=this.fieldsets[t];if(!a)return!1;let u=r.content;const c=i(a),d=i(l);for(const[p,h]of Object.entries(c)){const t=d[p];(null==t?void 0:t.type)===h.type&&(null==(s=null==o?void 0:o.content)?void 0:s[p])&&(u[p]=o.content[p])}this.blocks[n]={...r,id:o.id,content:u},this.save()},deselectAll(){this.batch=[],this.current=null},async duplicate(t,e){const s={...this.$helper.clone(t),id:this.$helper.uuid()};this.blocks.splice(e+1,0,s),this.save()},fieldset(t){return this.fieldsets[t.type]||{icon:"box",name:t.type,tabs:{content:{fields:{}}},type:t.type}},find(t){return this.blocks.find((e=>e.id===t))},findIndex(t){return this.blocks.findIndex((e=>e.id===t))},focus(t){(null==t?void 0:t.id)&&this.$refs["block-"+t.id]?this.$refs["block-"+t.id][0].focus():this.blocks[0]&&this.focus(this.blocks[0])},focusOrOpen(t){this.fieldsets[t.type].wysiwyg?this.focus(t):this.open(t)},hide(t){this.$set(t,"isHidden",!0),this.save()},isBatched(t){return this.batch.includes(t.id)},isInputEvent(){const t=document.querySelector(":focus");return t&&t.matches("input, textarea, [contenteditable], .k-writer")},isLastInBatch(t){const[e]=this.batch.slice(-1);return e&&t.id===e},isOnlyInstance:()=>1===document.querySelectorAll(".k-blocks").length,isSelected(t){return this.selected&&this.selected===t.id},move(t){if(t.from!==t.to){const e=t.draggedContext.element,s=t.relatedContext.component.componentData||t.relatedContext.component.$parent.componentData;if(!1===Object.keys(s.fieldsets).includes(e.type))return!1;if(!0===s.isFull)return!1}return!0},onBlur(){0===this.batch.length&&(this.isMultiSelectKey=!1)},onKey(t){this.isMultiSelectKey=t.metaKey||t.ctrlKey||t.altKey},onOutsideFocus(t){if("function"==typeof t.target.closest&&t.target.closest(".k-dialog"))return;const e=document.querySelector(".k-overlay:last-of-type");if(!1===this.$el.contains(t.target)&&(!e||!1===e.contains(t.target)))return this.select(null);if(e){const e=this.$el.closest(".k-layout-column");if(!1===(null==e?void 0:e.contains(t.target)))return this.select(null)}},onPaste(t){var e;return!0!==this.isInputEvent(t)&&(!0===this.isEditing?!0===(null==(e=this.$refs.selector)?void 0:e.isOpen())&&this.paste(t):(0!==this.selectedOrBatched.length||!0===this.isOnlyInstance())&&this.paste(t))},open(t){this.$refs["block-"+t.id]&&this.$refs["block-"+t.id][0].open()},async paste(t){const e=this.$helper.clipboard.read(t),s=await this.$api.post(this.endpoints.field+"/paste",{html:e});let n=this.selectedOrBatched[this.selectedOrBatched.length-1],i=this.findIndex(n);-1===i&&(i=this.blocks.length),this.append(s,i+1)},pasteboard(){this.$refs.pasteboard.open()},prevNext(t){if(this.blocks[t]){let e=this.blocks[t];if(this.$refs["block-"+e.id])return this.$refs["block-"+e.id][0]}},remove(t){var e;const s=this.findIndex(t.id);-1!==s&&((null==(e=this.selected)?void 0:e.id)===t.id&&this.select(null),this.$delete(this.blocks,s),this.save())},removeAll(){this.batch=[],this.blocks=[],this.save(),this.$refs.removeAll.close()},removeSelected(){this.batch.forEach((t=>{const e=this.findIndex(t);-1!==e&&this.$delete(this.blocks,e)})),this.deselectAll(),this.save(),this.$refs.removeSelected.close()},save(){this.$emit("input",this.blocks)},select(t,e=null){if(e&&this.isMultiSelectKey&&this.onKey(e),t&&this.isMultiSelectKey)return this.addToBatch(t),void(this.current=null);this.batch=[],this.current=t?t.id:null},selectAll(){this.batch=Object.values(this.blocks).map((t=>t.id))},show(t){this.$set(t,"isHidden",!1),this.save()},sort(t,e,s){if(s<0)return;let n=this.$helper.clone(this.blocks);n.splice(e,1),n.splice(s,0,t),this.blocks=n,this.save(),this.$nextTick((()=>{this.focus(t)}))},update(t,e){const s=this.findIndex(t.id);-1!==s&&Object.entries(e).forEach((([t,e])=>{this.$set(this.blocks[s].content,t,e)})),this.save()}}},(function(){var t=this,e=t._self._c;return e("div",{ref:"wrapper",staticClass:"k-blocks",attrs:{"data-empty":0===t.blocks.length,"data-multi-select-key":t.isMultiSelectKey},on:{focusin:function(e){t.focussed=!0},focusout:function(e){t.focussed=!1}}},[t.hasFieldsets?[e("k-draggable",t._b({staticClass:"k-blocks-list",on:{sort:t.save},scopedSlots:t._u([{key:"footer",fn:function(){return[e("k-empty",{staticClass:"k-blocks-empty",attrs:{icon:"box"},on:{click:function(e){return t.choose(t.blocks.length)}}},[t._v(" "+t._s(t.empty||t.$t("field.blocks.empty"))+" ")])]},proxy:!0}],null,!1,2413899928)},"k-draggable",t.draggableOptions,!1),t._l(t.blocks,(function(s,n){return e("k-block",t._b({key:s.id,ref:"block-"+s.id,refInFor:!0,attrs:{endpoints:t.endpoints,fieldset:t.fieldset(s),"is-batched":t.isBatched(s),"is-last-in-batch":t.isLastInBatch(s),"is-full":t.isFull,"is-hidden":!0===s.isHidden,"is-selected":t.isSelected(s),next:t.prevNext(n+1),prev:t.prevNext(n-1)},on:{append:function(e){return t.append(e,n+1)},blur:function(e){return t.select(null)},choose:function(e){return t.choose(e)},chooseToAppend:function(e){return t.choose(n+1)},chooseToConvert:function(e){return t.chooseToConvert(s)},chooseToPrepend:function(e){return t.choose(n)},copy:function(e){return t.copy()},confirmToRemoveSelected:t.confirmToRemoveSelected,duplicate:function(e){return t.duplicate(s,n)},focus:function(e){return t.select(s)},hide:function(e){return t.hide(s)},paste:function(e){return t.pasteboard()},prepend:function(e){return t.add(e,n)},remove:function(e){return t.remove(s)},sortDown:function(e){return t.sort(s,n,n+1)},sortUp:function(e){return t.sort(s,n,n-1)},show:function(e){return t.show(s)},update:function(e){return t.update(s,e)}},nativeOn:{click:function(e){return e.stopPropagation(),t.select(s,e)}}},"k-block",s,!1))})),1),e("k-block-selector",{ref:"selector",attrs:{fieldsets:t.fieldsets,"fieldset-groups":t.fieldsetGroups},on:{add:t.add,convert:t.convert,paste:function(e){return t.paste(e)}}}),e("k-remove-dialog",{ref:"removeAll",attrs:{text:t.$t("field.blocks.delete.confirm.all")},on:{submit:t.removeAll}}),e("k-remove-dialog",{ref:"removeSelected",attrs:{text:t.$t("field.blocks.delete.confirm.selected")},on:{submit:t.removeSelected}}),e("k-block-pasteboard",{ref:"pasteboard",on:{paste:function(e){return t.paste(e)}}})]:[e("k-box",{attrs:{theme:"info"}},[t._v(" No fieldsets yet ")])]],2)}),[],!1,null,null,null,null).exports;const vo=It({inheritAttrs:!1,props:{caption:String,captionMarks:[Boolean,Array],cover:{type:Boolean,default:!0},isEmpty:Boolean,emptyIcon:String,emptyText:String,ratio:String},computed:{ratioPadding(){return this.$helper.ratio(this.ratio||"16/9")}}},(function(){var t=this,e=t._self._c;return e("figure",{staticClass:"k-block-figure"},[t.isEmpty?e("k-button",{staticClass:"k-block-figure-empty",attrs:{icon:t.emptyIcon,text:t.emptyText},on:{click:function(e){return t.$emit("open")}}}):e("span",{staticClass:"k-block-figure-container",on:{dblclick:function(e){return t.$emit("open")}}},[t._t("default")],2),t.caption?e("figcaption",[e("k-writer",{attrs:{inline:!0,marks:t.captionMarks,value:t.caption},on:{input:function(e){return t.$emit("update",{caption:e})}}})],1):t._e()],1)}),[],!1,null,null,null,null).exports;const $o=It({props:{isBatched:Boolean,isEditable:Boolean,isFull:Boolean,isHidden:Boolean},methods:{open(){this.$refs.options.open()}}},(function(){var t=this,e=t._self._c;return e("k-dropdown",{staticClass:"k-block-options"},[t.isBatched?[e("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("copy"),icon:"template"},on:{click:function(e){return e.preventDefault(),t.$emit("copy")}}}),e("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("remove"),icon:"trash"},on:{click:function(e){return e.preventDefault(),t.$emit("confirmToRemoveSelected")}}})]:[t.isEditable?e("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("edit"),icon:"edit"},on:{click:function(e){return t.$emit("open")}}}):t._e(),e("k-button",{staticClass:"k-block-options-button",attrs:{disabled:t.isFull,tooltip:t.$t("insert.after"),icon:"add"},on:{click:function(e){return t.$emit("chooseToAppend")}}}),e("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("delete"),icon:"trash"},on:{click:function(e){return t.$emit("confirmToRemove")}}}),e("k-button",{staticClass:"k-block-options-button",attrs:{tooltip:t.$t("more"),icon:"dots"},on:{click:function(e){return t.$refs.options.toggle()}}}),e("k-button",{staticClass:"k-block-options-button k-sort-handle",attrs:{tooltip:t.$t("sort"),icon:"sort"},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:(e.preventDefault(),t.$emit("sortUp"))},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:(e.preventDefault(),t.$emit("sortDown"))}]}}),e("k-dropdown-content",{ref:"options",attrs:{align:"right"}},[e("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"angle-up"},on:{click:function(e){return t.$emit("chooseToPrepend")}}},[t._v(" "+t._s(t.$t("insert.before"))+" ")]),e("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"angle-down"},on:{click:function(e){return t.$emit("chooseToAppend")}}},[t._v(" "+t._s(t.$t("insert.after"))+" ")]),e("hr"),t.isEditable?e("k-dropdown-item",{attrs:{icon:"edit"},on:{click:function(e){return t.$emit("open")}}},[t._v(" "+t._s(t.$t("edit"))+" ")]):t._e(),e("k-dropdown-item",{attrs:{icon:"refresh"},on:{click:function(e){return t.$emit("chooseToConvert")}}},[t._v(" "+t._s(t.$t("field.blocks.changeType"))+" ")]),e("hr"),e("k-dropdown-item",{attrs:{icon:"template"},on:{click:function(e){return t.$emit("copy")}}},[t._v(" "+t._s(t.$t("copy"))+" ")]),e("k-dropdown-item",{attrs:{icon:"download"},on:{click:function(e){return t.$emit("paste")}}},[t._v(" "+t._s(t.$t("paste.after"))+" ")]),e("hr"),e("k-dropdown-item",{attrs:{icon:t.isHidden?"preview":"hidden"},on:{click:function(e){return t.$emit(t.isHidden?"show":"hide")}}},[t._v(" "+t._s(!0===t.isHidden?t.$t("show"):t.$t("hide"))+" ")]),e("k-dropdown-item",{attrs:{disabled:t.isFull,icon:"copy"},on:{click:function(e){return t.$emit("duplicate")}}},[t._v(" "+t._s(t.$t("duplicate"))+" ")]),e("hr"),e("k-dropdown-item",{attrs:{icon:"trash"},on:{click:function(e){return t.$emit("confirmToRemove")}}},[t._v(" "+t._s(t.$t("delete"))+" ")])],1)]],2)}),[],!1,null,null,null,null).exports;const _o=It({inheritAttrs:!1,props:{endpoint:String,fieldsets:Object,fieldsetGroups:Object},data(){return{dialogIsOpen:!1,disabled:[],headline:null,payload:null,event:"add",groups:this.createGroups()}},computed:{shortcut(){return this.$helper.keyboard.metaKey()+"+v"}},methods:{add(t){this.$emit(this.event,t,this.payload),this.$refs.dialog.close()},close(){this.$refs.dialog.close()},createGroups(){let t={},e=0;const s=this.fieldsetGroups||{blocks:{label:this.$t("field.blocks.fieldsets.label"),sets:Object.keys(this.fieldsets)}};return Object.keys(s).forEach((n=>{let i=s[n];i.open=!1!==i.open,i.fieldsets=i.sets.filter((t=>this.fieldsets[t])).map((t=>(e++,{...this.fieldsets[t],index:e}))),0!==i.fieldsets.length&&(t[n]=i)})),t},isOpen(){return this.dialogIsOpen},navigate(t){var e,s;null==(s=null==(e=this.$refs["fieldset-"+t])?void 0:e[0])||s.focus()},onClose(){this.dialogIsOpen=!1,this.$events.$off("paste",this.close)},onOpen(){this.dialogIsOpen=!0,this.$events.$on("paste",this.close)},open(t,e={}){const s={event:"add",disabled:[],headline:null,...e};this.event=s.event,this.disabled=s.disabled,this.headline=s.headline,this.payload=t,this.$refs.dialog.open()}}},(function(){var t=this,e=t._self._c;return e("k-dialog",{ref:"dialog",staticClass:"k-block-selector",attrs:{"cancel-button":!1,"submit-button":!1,size:"medium"},on:{open:t.onOpen,close:t.onClose}},[t.headline?e("k-headline",[t._v(" "+t._s(t.headline)+" ")]):t._e(),t._l(t.groups,(function(s,n){return e("details",{key:n,attrs:{open:s.open}},[e("summary",[t._v(t._s(s.label))]),e("div",{staticClass:"k-block-types"},t._l(s.fieldsets,(function(s){return e("k-button",{key:s.name,ref:"fieldset-"+s.index,refInFor:!0,attrs:{disabled:t.disabled.includes(s.type),icon:s.icon||"box",text:s.name},on:{keydown:[function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"up",38,e.key,["Up","ArrowUp"])?null:t.navigate(s.index-1)},function(e){return!e.type.indexOf("key")&&t._k(e.keyCode,"down",40,e.key,["Down","ArrowDown"])?null:t.navigate(s.index+1)}],click:function(e){return t.add(s.type)}}})})),1)])})),e("p",{staticClass:"k-clipboard-hint",domProps:{innerHTML:t._s(t.$t("field.blocks.fieldsets.paste",{shortcut:t.shortcut}))}})],2)}),[],!1,null,null,null,null).exports;const xo=It({inheritAttrs:!1,props:{fieldset:Object,content:Object},computed:{icon(){return this.fieldset.icon||"box"},label(){if(!this.fieldset.label||0===this.fieldset.label.length)return!1;if(this.fieldset.label===this.fieldset.name)return!1;const t=this.$helper.string.template(this.fieldset.label,this.content);return"…"!==t&&t},name(){return this.fieldset.name}}},(function(){var t=this,e=t._self._c;return e("div",t._g({staticClass:"k-block-title"},t.$listeners),[e("k-icon",{staticClass:"k-block-icon",attrs:{type:t.icon}}),e("span",{staticClass:"k-block-name"},[t._v(" "+t._s(t.name)+" ")]),t.label?e("span",{staticClass:"k-block-label"},[t._v(" "+t._s(t.label)+" ")]):t._e()],1)}),[],!1,null,null,null,null).exports;const wo=It({inheritAttrs:!1,props:{content:[Object,Array],fieldset:Object},methods:{field(t,e=null){let s=null;return Object.values(this.fieldset.tabs).forEach((e=>{e.fields[t]&&(s=e.fields[t])})),s||e},open(){this.$emit("open")},update(t){this.$emit("update",{...this.content,...t})}}},null,null,!1,null,null,null,null).exports;t.component("k-block",bo),t.component("k-blocks",yo),t.component("k-block-figure",vo),t.component("k-block-options",$o),t.component("k-block-selector",_o),t.component("k-block-title",xo),t.component("k-block-type",wo);const So=Object.assign({"./Types/Code.vue":Ji,"./Types/Default.vue":Wi,"./Types/Gallery.vue":Zi,"./Types/Heading.vue":to,"./Types/Image.vue":so,"./Types/Line.vue":io,"./Types/List.vue":ro,"./Types/Markdown.vue":ao,"./Types/Quote.vue":co,"./Types/Table.vue":ho,"./Types/Text.vue":fo,"./Types/Video.vue":ko});Object.keys(So).map((e=>{const s=e.match(/\/([a-zA-Z]*)\.vue/)[1].toLowerCase();let n=So[e].default;n.extends=wo,t.component("k-block-type-"+s,n)}));const Co={inheritAttrs:!1,props:{column:{type:Object,default:()=>({})},field:Object,value:{}}};const Oo=It({mixins:[Co],inheritAttrs:!1,props:{value:[Array,String]},computed:{bubbles(){var t,e;let s=this.value;const n=(null==(t=this.column)?void 0:t.options)||(null==(e=this.field)?void 0:e.options)||[];return"string"==typeof s&&(s=s.split(",")),s.map((t=>{"string"==typeof t&&(t={value:t,text:t});for(const e of n)e.value===t.value&&(t.text=e.text);return{back:"light",color:"black",...t}}))}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-bubbles-field-preview",class:t.$options.class},[e("k-bubbles",{attrs:{bubbles:t.bubbles}})],1)}),[],!1,null,null,null,null).exports;const Ao=It({extends:Oo,inheritAttrs:!1,class:"k-array-field-preview",computed:{bubbles(){return[{text:1===this.value.length?`1 ${this.$t("entry")}`:`${this.value.length} ${this.$t("entries")}`}]}}},null,null,!1,null,null,null,null).exports;const To=It({mixins:[Co],inheritAttrs:!1,computed:{text(){return this.value}}},(function(){var t=this;return(0,t._self._c)("p",{staticClass:"k-text-field-preview",class:t.$options.class},[t._v(" "+t._s(t.column.before)+" "),t._t("default",(function(){return[t._v(t._s(t.text))]})),t._v(" "+t._s(t.column.after)+" ")],2)}),[],!1,null,null,null,null).exports;const Io=It({extends:To,inheritAttrs:!1,props:{value:String},class:"k-date-field-preview",computed:{text(){var t,e,s,n,i,o;if("string"!=typeof this.value)return"";const r=this.$library.dayjs(this.value);if(!r)return"";let l=(null==(t=this.column)?void 0:t.display)||(null==(e=this.field)?void 0:e.display)||"YYYY-MM-DD",a=(null==(n=null==(s=this.column)?void 0:s.time)?void 0:n.display)||(null==(o=null==(i=this.field)?void 0:i.time)?void 0:o.display);return a&&(l+=" "+a),r.format(l)}}},null,null,!1,null,null,null,null).exports;const Mo=It({mixins:[Co],props:{value:[String,Object]},computed:{link(){return"object"==typeof this.value?this.value.href:this.value},text(){return"object"==typeof this.value?this.value.text:this.link}}},(function(){var t=this,e=t._self._c;return e("p",{staticClass:"k-url-field-preview",class:t.$options.class},[t._v(" "+t._s(t.column.before)+" "),e("k-link",{attrs:{to:t.link},nativeOn:{click:function(t){t.stopPropagation()}}},[t._v(" "+t._s(t.text)+" ")]),t._v(" "+t._s(t.column.after)+" ")],1)}),[],!1,null,null,null,null).exports;const Eo=It({extends:Mo,class:"k-email-field-preview"},null,null,!1,null,null,null,null).exports;const Lo=It({extends:Oo,inheritAttrs:!1,class:"k-files-field-preview",computed:{bubbles(){return this.value.map((t=>({text:t.filename,link:t.link,image:t.image})))}}},null,null,!1,null,null,null,null).exports;const jo=It({mixins:[Co],inheritAttrs:!1,props:{value:Object}},(function(){var t=this;return(0,t._self._c)("k-status-icon",t._b({staticClass:"k-flag-field-preview"},"k-status-icon",t.value,!1))}),[],!1,null,null,null,null).exports;const Do=It({mixins:[Co],inheritAttrs:!1,props:{value:String},computed:{html(){return this.value}}},(function(){var t=this,e=t._self._c;return e("div",{staticClass:"k-html-field-preview",class:t.$options.class},[t._v(" "+t._s(t.column.before)+" "),e("div",{domProps:{innerHTML:t._s(t.html)}}),t._v(" "+t._s(t.column.after)+" ")])}),[],!1,null,null,null,null).exports;const Bo=It({mixins:[Co],inheritAttrs:!1,props:{value:[Object]}},(function(){return(0,this._self._c)("k-item-image",{staticClass:"k-image-field-preview",attrs:{image:this.value,layout:"list"}})}),[],!1,null,null,null,null).exports;const Po=It({extends:Oo,inheritAttrs:!1,class:"k-pages-field-preview"},null,null,!1,null,null,null,null).exports;const No=It({extends:To,inheritAttrs:!1,props:{value:String},class:"k-time-field-preview",computed:{text(){const t=this.$library.dayjs.iso(this.value,"time");return(null==t?void 0:t.format(this.field.display))||""}}},null,null,!1,null,null,null,null).exports;const qo=It({props:{field:Object,value:Boolean,column:Object},computed:{text(){return!1!==this.column.text?this.field.text:null}}},(function(){var t=this;return(0,t._self._c)("k-input",{staticClass:"k-toggle-field-preview",attrs:{text:t.text,value:t.value,type:"toggle"},on:{input:function(e){return t.$emit("input",e)}}})}),[],!1,null,null,null,null).exports;const Fo=It({extends:Oo,inheritAttrs:!1,class:"k-users-field-preview",computed:{bubble(){return this.value.map((t=>({text:t.username,link:t.link,image:t.image})))}}},null,null,!1,null,null,null,null).exports;t.component("k-array-field-preview",Ao),t.component("k-bubbles-field-preview",Oo),t.component("k-date-field-preview",Io),t.component("k-email-field-preview",Eo),t.component("k-files-field-preview",Lo),t.component("k-flag-field-preview",jo),t.component("k-html-field-preview",Do),t.component("k-image-field-preview",Bo),t.component("k-pages-field-preview",Po),t.component("k-text-field-preview",To),t.component("k-toggle-field-preview",qo),t.component("k-time-field-preview",No),t.component("k-url-field-preview",Mo),t.component("k-users-field-preview",Fo),t.component("k-list-field-preview",Do),t.component("k-writer-field-preview",Do),t.component("k-checkboxes-field-preview",Oo),t.component("k-multiselect-field-preview",Oo),t.component("k-radio-field-preview",Oo),t.component("k-select-field-preview",Oo),t.component("k-tags-field-preview",Oo),t.component("k-toggles-field-preview",Oo),t.component("k-dialog",Mt),t.component("k-error-dialog",Lt),t.component("k-fiber-dialog",jt),t.component("k-files-dialog",Bt),t.component("k-form-dialog",Pt),t.component("k-language-dialog",Nt),t.component("k-pages-dialog",qt),t.component("k-remove-dialog",Ft),t.component("k-text-dialog",Rt),t.component("k-users-dialog",zt),t.component("k-drawer",Yt),t.component("k-form-drawer",Ht),t.component("k-calendar",Kt),t.component("k-counter",Gt),t.component("k-autocomplete",Ut),t.component("k-form",Jt),t.component("k-form-buttons",Vt),t.component("k-form-indicator",Wt),t.component("k-field",ae),t.component("k-fieldset",ue),t.component("k-input",de),t.component("k-login",pe),t.component("k-login-code",he),t.component("k-times",me),t.component("k-upload",fe),t.component("k-writer",We),t.component("k-login-alert",Xe),t.component("k-structure-form",Ze),t.component("k-toolbar",ts),t.component("k-toolbar-email-dialog",es),t.component("k-toolbar-link-dialog",ss),t.component("k-aspect-ratio",$n),t.component("k-bar",_n),t.component("k-box",xn),t.component("k-bubble",wn),t.component("k-bubbles",Sn),t.component("k-collection",Cn),t.component("k-column",On),t.component("k-dropzone",An),t.component("k-empty",Tn),t.component("k-file-preview",In),t.component("k-grid",Mn),t.component("k-header",En),t.component("k-inside",Ln),t.component("k-item",jn),t.component("k-item-image",Dn),t.component("k-items",Bn),t.component("k-overlay",Pn),t.component("k-panel",Nn),t.component("k-stats",qn),t.component("k-table",Fn),t.component("k-table-cell",Rn),t.component("k-tabs",zn),t.component("k-view",Yn),t.component("k-draggable",Un),t.component("k-error-boundary",Kn),t.component("k-fatal",Gn),t.component("k-headline",Jn),t.component("k-icon",Vn),t.component("k-icons",Wn),t.component("k-image",Xn),t.component("k-loader",Zn),t.component("k-offline-warning",Qn),t.component("k-progress",ei),t.component("k-registration",si),t.component("k-status-icon",ii),t.component("k-sort-handle",ni),t.component("k-text",oi),t.component("k-user-info",ri),t.component("k-breadcrumb",li),t.component("k-button",ai),t.component("k-button-disabled",ui),t.component("k-button-group",ci),t.component("k-button-link",di),t.component("k-button-native",hi),t.component("k-dropdown",mi),t.component("k-dropdown-content",gi),t.component("k-dropdown-item",ki),t.component("k-languages-dropdown",yi),t.component("k-link",bi),t.component("k-options-dropdown",vi),t.component("k-pagination",$i),t.component("k-prev-next",_i),t.component("k-search",xi),t.component("k-tag",wi),t.component("k-topbar",Si),t.component("k-account-view",Di),t.component("k-error-view",Bi),t.component("k-file-view",Pi),t.component("k-installation-view",Ni),t.component("k-languages-view",qi),t.component("k-login-view",Fi),t.component("k-page-view",Ri),t.component("k-plugin-view",zi),t.component("k-reset-password-view",Yi),t.component("k-site-view",Hi),t.component("k-system-view",Ui),t.component("k-users-view",Ki),t.component("k-user-view",ji);t.config.productionTip=!1,t.config.devtools=!0,t.use(tt),t.use(St),t.use(Ot),t.use(Tt),t.use(et),t.use(Ct),t.use(at),t.use(H,Q),t.use(N),t.use(q),new t({store:Q,created(){window.panel.$vue=window.panel.app=this,window.panel.plugins.created.forEach((t=>t(this))),this.$store.dispatch("content/init")},render:t=>t(U)}).$mount("#app"); diff --git a/kirby/panel/dist/js/vendor.js b/kirby/panel/dist/js/vendor.js index b552653..e918936 100644 --- a/kirby/panel/dist/js/vendor.js +++ b/kirby/panel/dist/js/vendor.js @@ -1,19 +1,12 @@ /*! - * Vue.js v2.6.14 - * (c) 2014-2021 Evan You + * Vue.js v2.7.10 + * (c) 2014-2022 Evan You * Released under the MIT License. */ -var t=Object.freeze({});function e(t){return null==t}function n(t){return null!=t}function r(t){return!0===t}function o(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function i(t){return null!==t&&"object"==typeof t}var a=Object.prototype.toString;function s(t){return"[object Object]"===a.call(t)}function c(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function l(t){return n(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function u(t){return null==t?"":Array.isArray(t)||s(t)&&t.toString===a?JSON.stringify(t,null,2):String(t)}function f(t){var e=parseFloat(t);return isNaN(e)?t:e}function p(t,e){for(var n=Object.create(null),r=t.split(","),o=0;o-1)return t.splice(n,1)}}var m=Object.prototype.hasOwnProperty;function g(t,e){return m.call(t,e)}function y(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var b=/-(\w)/g,w=y((function(t){return t.replace(b,(function(t,e){return e?e.toUpperCase():""}))})),x=y((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),S=/\B([A-Z])/g,k=y((function(t){return t.replace(S,"-$1").toLowerCase()}));var _=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var r=arguments.length;return r?r>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function O(t,e){e=e||0;for(var n=t.length-e,r=new Array(n);n--;)r[n]=t[n+e];return r}function C(t,e){for(var n in e)t[n]=e[n];return t}function M(t){for(var e={},n=0;n0,Y=H&&H.indexOf("edge/")>0;H&&H.indexOf("android");var U=H&&/iphone|ipad|ipod|ios/.test(H)||"ios"===q;H&&/chrome\/\d+/.test(H),H&&/phantomjs/.test(H);var X,G=H&&H.match(/firefox\/(\d+)/),Z={}.watch,Q=!1;if(V)try{var tt={};Object.defineProperty(tt,"passive",{get:function(){Q=!0}}),window.addEventListener("test-passive",null,tt)}catch(My){}var et=function(){return void 0===X&&(X=!V&&!W&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),X},nt=V&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function rt(t){return"function"==typeof t&&/native code/.test(t.toString())}var ot,it="undefined"!=typeof Symbol&&rt(Symbol)&&"undefined"!=typeof Reflect&&rt(Reflect.ownKeys);ot="undefined"!=typeof Set&&rt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var at=D,st=0,ct=function(){this.id=st++,this.subs=[]};ct.prototype.addSub=function(t){this.subs.push(t)},ct.prototype.removeSub=function(t){v(this.subs,t)},ct.prototype.depend=function(){ct.target&&ct.target.addDep(this)},ct.prototype.notify=function(){for(var t=this.subs.slice(),e=0,n=t.length;e-1)if(i&&!g(o,"default"))a=!1;else if(""===a||a===k(t)){var c=Lt(String,o.type);(c<0||s0&&(ue((s=fe(s,(i||"")+"_"+a))[0])&&ue(l)&&(u[c]=vt(l.text+s[0].text),s.shift()),u.push.apply(u,s)):o(s)?ue(l)?u[c]=vt(l.text+s):""!==s&&u.push(vt(s)):ue(s)&&ue(l)?u[c]=vt(l.text+s.text):(r(t._isVList)&&n(s.tag)&&e(s.key)&&n(i)&&(s.key="__vlist"+i+"_"+a+"__"),u.push(s)));return u}function pe(t,e){if(t){for(var n=Object.create(null),r=it?Reflect.ownKeys(t):Object.keys(t),o=0;o0,a=e?!!e.$stable:!i,s=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(a&&r&&r!==t&&s===r.$key&&!i&&!r.$hasNormal)return r;for(var c in o={},e)e[c]&&"$"!==c[0]&&(o[c]=ge(n,c,e[c]))}else o={};for(var l in n)l in o||(o[l]=ye(n,l));return e&&Object.isExtensible(e)&&(e._normalized=o),F(o,"$stable",a),F(o,"$key",s),F(o,"$hasNormal",i),o}function ge(t,e,n){var r=function(){var t=arguments.length?n.apply(null,arguments):n({}),e=(t=t&&"object"==typeof t&&!Array.isArray(t)?[t]:le(t))&&t[0];return t&&(!e||1===t.length&&e.isComment&&!ve(e))?void 0:t};return n.proxy&&Object.defineProperty(t,e,{get:r,enumerable:!0,configurable:!0}),r}function ye(t,e){return function(){return t[e]}}function be(t,e){var r,o,a,s,c;if(Array.isArray(t)||"string"==typeof t)for(r=new Array(t.length),o=0,a=t.length;odocument.createEvent("Event").timeStamp&&(fn=function(){return pn.now()})}function dn(){var t,e;for(un=fn(),cn=!0,rn.sort((function(t,e){return t.id-e.id})),ln=0;lnln&&rn[n].id>t.id;)n--;rn.splice(n+1,0,t)}else rn.push(t);sn||(sn=!0,te(dn))}}(this)},vn.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||i(t)||this.deep){var e=this.value;if(this.value=t,this.user){var n='callback for watcher "'+this.expression+'"';Vt(this.cb,this.vm,[t,e],this.vm,n)}else this.cb.call(this.vm,t,e)}}},vn.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},vn.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},vn.prototype.teardown=function(){if(this.active){this.vm._isBeingDestroyed||v(this.vm._watchers,this);for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1}};var mn={enumerable:!0,configurable:!0,get:D,set:D};function gn(t,e,n){mn.get=function(){return this[e][n]},mn.set=function(t){this[e][n]=t},Object.defineProperty(t,n,mn)}function yn(t){t._watchers=[];var e=t.$options;e.props&&function(t,e){var n=t.$options.propsData||{},r=t._props={},o=t.$options._propKeys=[];t.$parent&&xt(!1);var i=function(i){o.push(i);var a=jt(i,e,n,t);_t(r,i,a),i in t||gn(t,"_props",i)};for(var a in e)i(a);xt(!0)}(t,e.props),e.methods&&function(t,e){for(var n in t.$options.props,e)t[n]="function"!=typeof e[n]?D:_(e[n],t)}(t,e.methods),e.data?function(t){var e=t.$options.data;s(e=t._data="function"==typeof e?function(t,e){ut();try{return t.call(e,e)}catch(My){return Bt(My,e,"data()"),{}}finally{ft()}}(e,t):e||{})||(e={});var n=Object.keys(e),r=t.$options.props;t.$options.methods;var o=n.length;for(;o--;){var i=n[o];r&&g(r,i)||z(i)||gn(t,"_data",i)}kt(e,!0)}(t):kt(t._data={},!0),e.computed&&function(t,e){var n=t._computedWatchers=Object.create(null),r=et();for(var o in e){var i=e[o],a="function"==typeof i?i:i.get;r||(n[o]=new vn(t,a||D,D,bn)),o in t||wn(t,o,i)}}(t,e.computed),e.watch&&e.watch!==Z&&function(t,e){for(var n in e){var r=e[n];if(Array.isArray(r))for(var o=0;o-1:"string"==typeof t?t.split(",").indexOf(e)>-1:(n=t,"[object RegExp]"===a.call(n)&&t.test(e));var n}function $n(t,e){var n=t.cache,r=t.keys,o=t._vnode;for(var i in n){var a=n[i];if(a){var s=a.name;s&&!e(s)&&An(n,i,r,o)}}}function An(t,e,n,r){var o=t[e];!o||r&&o.tag===r.tag||o.componentInstance.$destroy(),t[e]=null,v(n,e)}Cn.prototype._init=function(e){var n=this;n._uid=_n++,n._isVue=!0,e&&e._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),r=e._parentVnode;n.parent=e.parent,n._parentVnode=r;var o=r.componentOptions;n.propsData=o.propsData,n._parentListeners=o.listeners,n._renderChildren=o.children,n._componentTag=o.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(n,e):n.$options=Pt(On(n.constructor),e||{},n),n._renderProxy=n,n._self=n,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(n),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Xe(t,e)}(n),function(e){e._vnode=null,e._staticTrees=null;var n=e.$options,r=e.$vnode=n._parentVnode,o=r&&r.context;e.$slots=de(n._renderChildren,o),e.$scopedSlots=t,e._c=function(t,n,r,o){return Be(e,t,n,r,o,!1)},e.$createElement=function(t,n,r,o){return Be(e,t,n,r,o,!0)};var i=r&&r.data;_t(e,"$attrs",i&&i.attrs||t,null,!0),_t(e,"$listeners",n._parentListeners||t,null,!0)}(n),nn(n,"beforeCreate"),function(t){var e=pe(t.$options.inject,t);e&&(xt(!1),Object.keys(e).forEach((function(n){_t(t,n,e[n])})),xt(!0))}(n),yn(n),function(t){var e=t.$options.provide;e&&(t._provided="function"==typeof e?e.call(t):e)}(n),nn(n,"created"),n.$options.el&&n.$mount(n.$options.el)},function(t){var e={get:function(){return this._data}},n={get:function(){return this._props}};Object.defineProperty(t.prototype,"$data",e),Object.defineProperty(t.prototype,"$props",n),t.prototype.$set=Ot,t.prototype.$delete=Ct,t.prototype.$watch=function(t,e,n){var r=this;if(s(e))return kn(r,t,e,n);(n=n||{}).user=!0;var o=new vn(r,t,e,n);if(n.immediate){var i='callback for immediate watcher "'+o.expression+'"';ut(),Vt(e,r,[o.value],r,i),ft()}return function(){o.teardown()}}}(Cn),function(t){var e=/^hook:/;t.prototype.$on=function(t,n){var r=this;if(Array.isArray(t))for(var o=0,i=t.length;o1?O(n):n;for(var r=O(arguments,1),o='event handler for "'+t+'"',i=0,a=n.length;iparseInt(this.max)&&An(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)An(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){$n(t,(function(t){return Tn(e,t)}))})),this.$watch("exclude",(function(e){$n(t,(function(t){return!Tn(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Je(t),n=e&&e.componentOptions;if(n){var r=Dn(n),o=this.include,i=this.exclude;if(o&&(!r||!Tn(o,r))||i&&r&&Tn(i,r))return e;var a=this.cache,s=this.keys,c=null==e.key?n.Ctor.cid+(n.tag?"::"+n.tag:""):e.key;a[c]?(e.componentInstance=a[c].componentInstance,v(s,c),s.push(c)):(this.vnodeToCache=e,this.keyToCache=c),e.data.keepAlive=!0}return e||t&&t[0]}},Pn={KeepAlive:Nn};!function(t){var e={get:function(){return j}};Object.defineProperty(t,"config",e),t.util={warn:at,extend:C,mergeOptions:Pt,defineReactive:_t},t.set=Ot,t.delete=Ct,t.nextTick=te,t.observable=function(t){return kt(t),t},t.options=Object.create(null),P.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,C(t.options.components,Pn),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=O(arguments,1);return n.unshift(this),"function"==typeof t.install?t.install.apply(t,n):"function"==typeof t&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Pt(this.options,t),this}}(t),Mn(t),function(t){P.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&s(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&"function"==typeof n&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(Cn),Object.defineProperty(Cn.prototype,"$isServer",{get:et}),Object.defineProperty(Cn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Cn,"FunctionalRenderContext",{value:Pe}),Cn.version="2.6.14";var In=p("style,class"),jn=p("input,textarea,option,select,progress"),Rn=function(t,e,n){return"value"===n&&jn(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},zn=p("contenteditable,draggable,spellcheck"),Fn=p("events,caret,typing,plaintext-only"),Ln=p("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),Bn="http://www.w3.org/1999/xlink",Vn=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Wn=function(t){return Vn(t)?t.slice(6,t.length):""},qn=function(t){return null==t||!1===t};function Hn(t){for(var e=t.data,r=t,o=t;n(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=Jn(o.data,e));for(;n(r=r.parent);)r&&r.data&&(e=Jn(e,r.data));return function(t,e){if(n(t)||n(e))return Kn(t,Yn(e));return""}(e.staticClass,e.class)}function Jn(t,e){return{staticClass:Kn(t.staticClass,e.staticClass),class:n(t.class)?[t.class,e.class]:e.class}}function Kn(t,e){return t?e?t+" "+e:t:e||""}function Yn(t){return Array.isArray(t)?function(t){for(var e,r="",o=0,i=t.length;o-1?br(t,e,n):Ln(e)?qn(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):zn(e)?t.setAttribute(e,function(t,e){return qn(e)||"false"===e?"false":"contenteditable"===t&&Fn(e)?e:"true"}(e,n)):Vn(e)?qn(n)?t.removeAttributeNS(Bn,Wn(e)):t.setAttributeNS(Bn,e,n):br(t,e,n)}function br(t,e,n){if(qn(n))t.removeAttribute(e);else{if(J&&!K&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var wr={create:gr,update:gr};function xr(t,r){var o=r.elm,i=r.data,a=t.data;if(!(e(i.staticClass)&&e(i.class)&&(e(a)||e(a.staticClass)&&e(a.class)))){var s=Hn(r),c=o._transitionClasses;n(c)&&(s=Kn(s,Yn(c))),s!==o._prevClass&&(o.setAttribute("class",s),o._prevClass=s)}}var Sr,kr,_r,Or,Cr,Mr,Dr={create:xr,update:xr},Tr=/[\w).+\-_$\]]/;function $r(t){var e,n,r,o,i,a=!1,s=!1,c=!1,l=!1,u=0,f=0,p=0,d=0;for(r=0;r=0&&" "===(v=t.charAt(h));h--);v&&Tr.test(v)||(l=!0)}}else void 0===o?(d=r+1,o=t.slice(0,r).trim()):m();function m(){(i||(i=[])).push(t.slice(d,r).trim()),d=r+1}if(void 0===o?o=t.slice(0,r).trim():0!==d&&m(),i)for(r=0;r-1?{exp:t.slice(0,Or),key:'"'+t.slice(Or+1)+'"'}:{exp:t,key:null};kr=t,Or=Cr=Mr=0;for(;!Kr();)Yr(_r=Jr())?Xr(_r):91===_r&&Ur(_r);return{exp:t.slice(0,Cr),key:t.slice(Cr+1,Mr)}}(t);return null===n.key?t+"="+e:"$set("+n.exp+", "+n.key+", "+e+")"}function Jr(){return kr.charCodeAt(++Or)}function Kr(){return Or>=Sr}function Yr(t){return 34===t||39===t}function Ur(t){var e=1;for(Cr=Or;!Kr();)if(Yr(t=Jr()))Xr(t);else if(91===t&&e++,93===t&&e--,0===e){Mr=Or;break}}function Xr(t){for(var e=t;!Kr()&&(t=Jr())!==e;);}var Gr;function Zr(t,e,n){var r=Gr;return function o(){var i=e.apply(null,arguments);null!==i&&eo(t,o,n,r)}}var Qr=Jt&&!(G&&Number(G[1])<=53);function to(t,e,n,r){if(Qr){var o=un,i=e;e=i._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=o||t.timeStamp<=0||t.target.ownerDocument!==document)return i.apply(this,arguments)}}Gr.addEventListener(t,e,Q?{capture:n,passive:r}:n)}function eo(t,e,n,r){(r||Gr).removeEventListener(t,e._wrapper||e,n)}function no(t,r){if(!e(t.data.on)||!e(r.data.on)){var o=r.data.on||{},i=t.data.on||{};Gr=r.elm,function(t){if(n(t.__r)){var e=J?"change":"input";t[e]=[].concat(t.__r,t[e]||[]),delete t.__r}n(t.__c)&&(t.change=[].concat(t.__c,t.change||[]),delete t.__c)}(o),ae(o,i,to,eo,Zr,r.context),Gr=void 0}}var ro,oo={create:no,update:no};function io(t,r){if(!e(t.data.domProps)||!e(r.data.domProps)){var o,i,a=r.elm,s=t.data.domProps||{},c=r.data.domProps||{};for(o in n(c.__ob__)&&(c=r.data.domProps=C({},c)),s)o in c||(a[o]="");for(o in c){if(i=c[o],"textContent"===o||"innerHTML"===o){if(r.children&&(r.children.length=0),i===s[o])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===o&&"PROGRESS"!==a.tagName){a._value=i;var l=e(i)?"":String(i);ao(a,l)&&(a.value=l)}else if("innerHTML"===o&&Gn(a.tagName)&&e(a.innerHTML)){(ro=ro||document.createElement("div")).innerHTML=""+i+"";for(var u=ro.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;u.firstChild;)a.appendChild(u.firstChild)}else if(i!==s[o])try{a[o]=i}catch(My){}}}}function ao(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(My){}return n&&t.value!==e}(t,e)||function(t,e){var r=t.value,o=t._vModifiers;if(n(o)){if(o.number)return f(r)!==f(e);if(o.trim)return r.trim()!==e.trim()}return r!==e}(t,e))}var so={create:io,update:io},co=y((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var r=t.split(n);r.length>1&&(e[r[0].trim()]=r[1].trim())}})),e}));function lo(t){var e=uo(t.style);return t.staticStyle?C(t.staticStyle,e):e}function uo(t){return Array.isArray(t)?M(t):"string"==typeof t?co(t):t}var fo,po=/^--/,ho=/\s*!important$/,vo=function(t,e,n){if(po.test(e))t.style.setProperty(e,n);else if(ho.test(n))t.style.setProperty(k(e),n.replace(ho,""),"important");else{var r=go(e);if(Array.isArray(n))for(var o=0,i=n.length;o-1?e.split(wo).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" "+(t.getAttribute("class")||"")+" ";n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function So(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(wo).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" "+(t.getAttribute("class")||"")+" ",r=" "+e+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function ko(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&C(e,_o(t.name||"v")),C(e,t),e}return"string"==typeof t?_o(t):void 0}}var _o=y((function(t){return{enterClass:t+"-enter",enterToClass:t+"-enter-to",enterActiveClass:t+"-enter-active",leaveClass:t+"-leave",leaveToClass:t+"-leave-to",leaveActiveClass:t+"-leave-active"}})),Oo=V&&!K,Co="transition",Mo="transitionend",Do="animation",To="animationend";Oo&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Co="WebkitTransition",Mo="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(Do="WebkitAnimation",To="webkitAnimationEnd"));var $o=V?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function Ao(t){$o((function(){$o(t)}))}function Eo(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),xo(t,e))}function No(t,e){t._transitionClasses&&v(t._transitionClasses,e),So(t,e)}function Po(t,e,n){var r=jo(t,e),o=r.type,i=r.timeout,a=r.propCount;if(!o)return n();var s="transition"===o?Mo:To,c=0,l=function(){t.removeEventListener(s,u),n()},u=function(e){e.target===t&&++c>=a&&l()};setTimeout((function(){c0&&(n="transition",u=a,f=i.length):"animation"===e?l>0&&(n="animation",u=l,f=c.length):f=(n=(u=Math.max(a,l))>0?a>l?"transition":"animation":null)?"transition"===n?i.length:c.length:0,{type:n,timeout:u,propCount:f,hasTransform:"transition"===n&&Io.test(r[Co+"Property"])}}function Ro(t,e){for(;t.length1}function Wo(t,e){!0!==e.data.show&&Fo(e)}var qo=function(t){var i,a,s={},c=t.modules,l=t.nodeOps;for(i=0;ih?b(t,e(o[g+1])?null:o[g+1].elm,o,d,g,i):d>g&&x(r,p,h)}(p,v,g,i,u):n(g)?(n(t.text)&&l.setTextContent(p,""),b(p,null,g,0,g.length-1,i)):n(v)?x(v,0,v.length-1):n(t.text)&&l.setTextContent(p,""):t.text!==o.text&&l.setTextContent(p,o.text),n(h)&&n(d=h.hook)&&n(d=d.postpatch)&&d(t,o)}}}function O(t,e,o){if(r(o)&&n(t.parent))t.parent.data.pendingInsert=e;else for(var i=0;i-1,a.selected!==i&&(a.selected=i);else if(A(Uo(a),r))return void(t.selectedIndex!==s&&(t.selectedIndex=s));o||(t.selectedIndex=-1)}}function Yo(t,e){return e.every((function(e){return!A(e,t)}))}function Uo(t){return"_value"in t?t._value:t.value}function Xo(t){t.target.composing=!0}function Go(t){t.target.composing&&(t.target.composing=!1,Zo(t.target,"input"))}function Zo(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function Qo(t){return!t.componentInstance||t.data&&t.data.transition?t:Qo(t.componentInstance._vnode)}var ti={model:Ho,show:{bind:function(t,e,n){var r=e.value,o=(n=Qo(n)).data&&n.data.transition,i=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&o?(n.data.show=!0,Fo(n,(function(){t.style.display=i}))):t.style.display=r?i:"none"},update:function(t,e,n){var r=e.value;!r!=!e.oldValue&&((n=Qo(n)).data&&n.data.transition?(n.data.show=!0,r?Fo(n,(function(){t.style.display=t.__vOriginalDisplay})):Lo(n,(function(){t.style.display="none"}))):t.style.display=r?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,r,o){o||(t.style.display=t.__vOriginalDisplay)}}},ei={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function ni(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?ni(Je(e.children)):t}function ri(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var o=n._parentListeners;for(var i in o)e[w(i)]=o[i];return e}function oi(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var ii=function(t){return t.tag||ve(t)},ai=function(t){return"show"===t.name},si={name:"transition",props:ei,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(ii)).length){var r=this.mode,i=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return i;var a=ni(i);if(!a)return i;if(this._leaving)return oi(t,i);var s="__transition-"+this._uid+"-";a.key=null==a.key?a.isComment?s+"comment":s+a.tag:o(a.key)?0===String(a.key).indexOf(s)?a.key:s+a.key:a.key;var c=(a.data||(a.data={})).transition=ri(this),l=this._vnode,u=ni(l);if(a.data.directives&&a.data.directives.some(ai)&&(a.data.show=!0),u&&u.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(a,u)&&!ve(u)&&(!u.componentInstance||!u.componentInstance._vnode.isComment)){var f=u.data.transition=C({},c);if("out-in"===r)return this._leaving=!0,se(f,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),oi(t,i);if("in-out"===r){if(ve(a))return l;var p,d=function(){p()};se(c,"afterEnter",d),se(c,"enterCancelled",d),se(f,"delayLeave",(function(t){p=t}))}}return i}}},ci=C({tag:String,moveClass:String},ei);delete ci.mode;var li={props:ci,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var o=Ze(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,o(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,o=this.$slots.default||[],i=this.children=[],a=ri(this),s=0;s-1?tr[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:tr[t]=/HTMLUnknownElement/.test(e.toString())},C(Cn.options.directives,ti),C(Cn.options.components,di),Cn.prototype.__patch__=V?qo:D,Cn.prototype.$mount=function(t,e){return function(t,e,n){var r;return t.$el=e,t.$options.render||(t.$options.render=ht),nn(t,"beforeMount"),r=function(){t._update(t._render(),n)},new vn(t,r,D,{before:function(){t._isMounted&&!t._isDestroyed&&nn(t,"beforeUpdate")}},!0),n=!1,null==t.$vnode&&(t._isMounted=!0,nn(t,"mounted")),t}(this,t=t&&V?nr(t):void 0,e)},V&&setTimeout((function(){j.devtools&&nt&&nt.emit("init",Cn)}),0);var hi=/\{\{((?:.|\r?\n)+?)\}\}/g,vi=/[-.*+?^${}()|[\]\/\\]/g,mi=y((function(t){var e=t[0].replace(vi,"\\$&"),n=t[1].replace(vi,"\\$&");return new RegExp(e+"((?:.|\\n)+?)"+n,"g")}));var gi={staticKeys:["staticClass"],transformNode:function(t,e){e.warn;var n=Br(t,"class");n&&(t.staticClass=JSON.stringify(n));var r=Lr(t,"class",!1);r&&(t.classBinding=r)},genData:function(t){var e="";return t.staticClass&&(e+="staticClass:"+t.staticClass+","),t.classBinding&&(e+="class:"+t.classBinding+","),e}};var yi,bi={staticKeys:["staticStyle"],transformNode:function(t,e){e.warn;var n=Br(t,"style");n&&(t.staticStyle=JSON.stringify(co(n)));var r=Lr(t,"style",!1);r&&(t.styleBinding=r)},genData:function(t){var e="";return t.staticStyle&&(e+="staticStyle:"+t.staticStyle+","),t.styleBinding&&(e+="style:("+t.styleBinding+"),"),e}},wi=function(t){return(yi=yi||document.createElement("div")).innerHTML=t,yi.textContent},xi=p("area,base,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr"),Si=p("colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source"),ki=p("address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track"),_i=/^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Oi=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Ci="[a-zA-Z_][\\-\\.0-9_a-zA-Z"+R.source+"]*",Mi="((?:"+Ci+"\\:)?"+Ci+")",Di=new RegExp("^<"+Mi),Ti=/^\s*(\/?)>/,$i=new RegExp("^<\\/"+Mi+"[^>]*>"),Ai=/^]+>/i,Ei=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},Ri=/&(?:lt|gt|quot|amp|#39);/g,zi=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,Fi=p("pre,textarea",!0),Li=function(t,e){return t&&Fi(t)&&"\n"===e[0]};function Bi(t,e){var n=e?zi:Ri;return t.replace(n,(function(t){return ji[t]}))}var Vi,Wi,qi,Hi,Ji,Ki,Yi,Ui,Xi=/^@|^v-on:/,Gi=/^v-|^@|^:|^#/,Zi=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,Qi=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,ta=/^\(|\)$/g,ea=/^\[.*\]$/,na=/:(.*)$/,ra=/^:|^\.|^v-bind:/,oa=/\.[^.\]]+(?=[^\]]*$)/g,ia=/^v-slot(:|$)|^#/,aa=/[\r\n]/,sa=/[ \f\t\r\n]+/g,ca=y(wi);function la(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:ma(e),rawAttrsMap:{},parent:n,children:[]}}function ua(t,e){Vi=e.warn||Er,Ki=e.isPreTag||T,Yi=e.mustUseProp||T,Ui=e.getTagNamespace||T,e.isReservedTag,qi=Nr(e.modules,"transformNode"),Hi=Nr(e.modules,"preTransformNode"),Ji=Nr(e.modules,"postTransformNode"),Wi=e.delimiters;var n,r,o=[],i=!1!==e.preserveWhitespace,a=e.whitespace,s=!1,c=!1;function l(t){if(u(t),s||t.processed||(t=fa(t,e)),o.length||t===n||n.if&&(t.elseif||t.else)&&da(n,{exp:t.elseif,block:t}),r&&!t.forbidden)if(t.elseif||t.else)a=t,l=function(t){for(var e=t.length;e--;){if(1===t[e].type)return t[e];t.pop()}}(r.children),l&&l.if&&da(l,{exp:a.elseif,block:a});else{if(t.slotScope){var i=t.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[i]=t}r.children.push(t),t.parent=r}var a,l;t.children=t.children.filter((function(t){return!t.slotScope})),u(t),t.pre&&(s=!1),Ki(t.tag)&&(c=!1);for(var f=0;f]*>)","i")),p=t.replace(f,(function(t,n,r){return l=r.length,Pi(u)||"noscript"===u||(n=n.replace(//g,"$1").replace(//g,"$1")),Li(u,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));c+=t.length-p.length,t=p,C(u,c-l,c)}else{var d=t.indexOf("<");if(0===d){if(Ei.test(t)){var h=t.indexOf("--\x3e");if(h>=0){e.shouldKeepComment&&e.comment(t.substring(4,h),c,c+h+3),k(h+3);continue}}if(Ni.test(t)){var v=t.indexOf("]>");if(v>=0){k(v+2);continue}}var m=t.match(Ai);if(m){k(m[0].length);continue}var g=t.match($i);if(g){var y=c;k(g[0].length),C(g[1],y,c);continue}var b=_();if(b){O(b),Li(b.tagName,t)&&k(1);continue}}var w=void 0,x=void 0,S=void 0;if(d>=0){for(x=t.slice(d);!($i.test(x)||Di.test(x)||Ei.test(x)||Ni.test(x)||(S=x.indexOf("<",1))<0);)d+=S,x=t.slice(d);w=t.substring(0,d)}d<0&&(w=t),w&&k(w.length),e.chars&&w&&e.chars(w,c-w.length,c)}if(t===n){e.chars&&e.chars(t);break}}function k(e){c+=e,t=t.substring(e)}function _(){var e=t.match(Di);if(e){var n,r,o={tagName:e[1],attrs:[],start:c};for(k(e[0].length);!(n=t.match(Ti))&&(r=t.match(Oi)||t.match(_i));)r.start=c,k(r[0].length),r.end=c,o.attrs.push(r);if(n)return o.unarySlash=n[1],k(n[0].length),o.end=c,o}}function O(t){var n=t.tagName,c=t.unarySlash;i&&("p"===r&&ki(n)&&C(r),s(n)&&r===n&&C(n));for(var l=a(n)||!!c,u=t.attrs.length,f=new Array(u),p=0;p=0&&o[a].lowerCasedTag!==s;a--);else a=0;if(a>=0){for(var l=o.length-1;l>=a;l--)e.end&&e.end(o[l].tag,n,i);o.length=a,r=a&&o[a-1].tag}else"br"===s?e.start&&e.start(t,[],!0,n,i):"p"===s&&(e.start&&e.start(t,[],!1,n,i),e.end&&e.end(t,n,i))}C()}(t,{warn:Vi,expectHTML:e.expectHTML,isUnaryTag:e.isUnaryTag,canBeLeftOpenTag:e.canBeLeftOpenTag,shouldDecodeNewlines:e.shouldDecodeNewlines,shouldDecodeNewlinesForHref:e.shouldDecodeNewlinesForHref,shouldKeepComment:e.comments,outputSourceRange:e.outputSourceRange,start:function(t,i,a,u,f){var p=r&&r.ns||Ui(t);J&&"svg"===p&&(i=function(t){for(var e=[],n=0;nc&&(s.push(i=t.slice(c,o)),a.push(JSON.stringify(i)));var l=$r(r[1].trim());a.push("_s("+l+")"),s.push({"@binding":l}),c=o+r[0].length}return c-1"+("true"===i?":("+e+")":":_q("+e+","+i+")")),Fr(t,"change","var $$a="+e+",$$el=$event.target,$$c=$$el.checked?("+i+"):("+a+");if(Array.isArray($$a)){var $$v="+(r?"_n("+o+")":o)+",$$i=_i($$a,$$v);if($$el.checked){$$i<0&&("+Hr(e,"$$a.concat([$$v])")+")}else{$$i>-1&&("+Hr(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))")+")}}else{"+Hr(e,"$$c")+"}",null,!0)}(t,r,o);else if("input"===i&&"radio"===a)!function(t,e,n){var r=n&&n.number,o=Lr(t,"value")||"null";Pr(t,"checked","_q("+e+","+(o=r?"_n("+o+")":o)+")"),Fr(t,"change",Hr(e,o),null,!0)}(t,r,o);else if("input"===i||"textarea"===i)!function(t,e,n){var r=t.attrsMap.type,o=n||{},i=o.lazy,a=o.number,s=o.trim,c=!i&&"range"!==r,l=i?"change":"range"===r?"__r":"input",u="$event.target.value";s&&(u="$event.target.value.trim()");a&&(u="_n("+u+")");var f=Hr(e,u);c&&(f="if($event.target.composing)return;"+f);Pr(t,"value","("+e+")"),Fr(t,l,f,null,!0),(s||a)&&Fr(t,"blur","$forceUpdate()")}(t,r,o);else if(!j.isReservedTag(i))return qr(t,r,o),!1;return!0},text:function(t,e){e.value&&Pr(t,"textContent","_s("+e.value+")",e)},html:function(t,e){e.value&&Pr(t,"innerHTML","_s("+e.value+")",e)}},Oa={expectHTML:!0,modules:wa,directives:_a,isPreTag:function(t){return"pre"===t},isUnaryTag:xi,mustUseProp:Rn,canBeLeftOpenTag:Si,isReservedTag:Zn,getTagNamespace:Qn,staticKeys:(xa=wa,xa.reduce((function(t,e){return t.concat(e.staticKeys||[])}),[]).join(","))},Ca=y((function(t){return p("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(t?","+t:""))}));function Ma(t,e){t&&(Sa=Ca(e.staticKeys||""),ka=e.isReservedTag||T,Da(t),Ta(t,!1))}function Da(t){if(t.static=function(t){if(2===t.type)return!1;if(3===t.type)return!0;return!(!t.pre&&(t.hasBindings||t.if||t.for||d(t.tag)||!ka(t.tag)||function(t){for(;t.parent;){if("template"!==(t=t.parent).tag)return!1;if(t.for)return!0}return!1}(t)||!Object.keys(t).every(Sa)))}(t),1===t.type){if(!ka(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var e=0,n=t.children.length;e|^function(?:\s+[\w$]+)?\s*\(/,Aa=/\([^)]*?\);*$/,Ea=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Na={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Pa={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},Ia=function(t){return"if("+t+")return null;"},ja={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:Ia("$event.target !== $event.currentTarget"),ctrl:Ia("!$event.ctrlKey"),shift:Ia("!$event.shiftKey"),alt:Ia("!$event.altKey"),meta:Ia("!$event.metaKey"),left:Ia("'button' in $event && $event.button !== 0"),middle:Ia("'button' in $event && $event.button !== 1"),right:Ia("'button' in $event && $event.button !== 2")};function Ra(t,e){var n=e?"nativeOn:":"on:",r="",o="";for(var i in t){var a=za(t[i]);t[i]&&t[i].dynamic?o+=i+","+a+",":r+='"'+i+'":'+a+","}return r="{"+r.slice(0,-1)+"}",o?n+"_d("+r+",["+o.slice(0,-1)+"])":n+r}function za(t){if(!t)return"function(){}";if(Array.isArray(t))return"["+t.map((function(t){return za(t)})).join(",")+"]";var e=Ea.test(t.value),n=$a.test(t.value),r=Ea.test(t.value.replace(Aa,""));if(t.modifiers){var o="",i="",a=[];for(var s in t.modifiers)if(ja[s])i+=ja[s],Na[s]&&a.push(s);else if("exact"===s){var c=t.modifiers;i+=Ia(["ctrl","shift","alt","meta"].filter((function(t){return!c[t]})).map((function(t){return"$event."+t+"Key"})).join("||"))}else a.push(s);return a.length&&(o+=function(t){return"if(!$event.type.indexOf('key')&&"+t.map(Fa).join("&&")+")return null;"}(a)),i&&(o+=i),"function($event){"+o+(e?"return "+t.value+".apply(null, arguments)":n?"return ("+t.value+").apply(null, arguments)":r?"return "+t.value:t.value)+"}"}return e||n?t.value:"function($event){"+(r?"return "+t.value:t.value)+"}"}function Fa(t){var e=parseInt(t,10);if(e)return"$event.keyCode!=="+e;var n=Na[t],r=Pa[t];return"_k($event.keyCode,"+JSON.stringify(t)+","+JSON.stringify(n)+",$event.key,"+JSON.stringify(r)+")"}var La={on:function(t,e){t.wrapListeners=function(t){return"_g("+t+","+e.value+")"}},bind:function(t,e){t.wrapData=function(n){return"_b("+n+",'"+t.tag+"',"+e.value+","+(e.modifiers&&e.modifiers.prop?"true":"false")+(e.modifiers&&e.modifiers.sync?",true":"")+")"}},cloak:D},Ba=function(t){this.options=t,this.warn=t.warn||Er,this.transforms=Nr(t.modules,"transformCode"),this.dataGenFns=Nr(t.modules,"genData"),this.directives=C(C({},La),t.directives);var e=t.isReservedTag||T;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function Va(t,e){var n=new Ba(e);return{render:"with(this){return "+(t?"script"===t.tag?"null":Wa(t,n):'_c("div")')+"}",staticRenderFns:n.staticRenderFns}}function Wa(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return qa(t,e);if(t.once&&!t.onceProcessed)return Ha(t,e);if(t.for&&!t.forProcessed)return Ya(t,e);if(t.if&&!t.ifProcessed)return Ja(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return function(t,e){var n=t.slotName||'"default"',r=Za(t,e),o="_t("+n+(r?",function(){return "+r+"}":""),i=t.attrs||t.dynamicAttrs?es((t.attrs||[]).concat(t.dynamicAttrs||[]).map((function(t){return{name:w(t.name),value:t.value,dynamic:t.dynamic}}))):null,a=t.attrsMap["v-bind"];!i&&!a||r||(o+=",null");i&&(o+=","+i);a&&(o+=(i?"":",null")+","+a);return o+")"}(t,e);var n;if(t.component)n=function(t,e,n){var r=e.inlineTemplate?null:Za(e,n,!0);return"_c("+t+","+Ua(e,n)+(r?","+r:"")+")"}(t.component,t,e);else{var r;(!t.plain||t.pre&&e.maybeComponent(t))&&(r=Ua(t,e));var o=t.inlineTemplate?null:Za(t,e,!0);n="_c('"+t.tag+"'"+(r?","+r:"")+(o?","+o:"")+")"}for(var i=0;i>>0}(a):"")+")"}(t,t.scopedSlots,e)+","),t.model&&(n+="model:{value:"+t.model.value+",callback:"+t.model.callback+",expression:"+t.model.expression+"},"),t.inlineTemplate){var i=function(t,e){var n=t.children[0];if(n&&1===n.type){var r=Va(n,e.options);return"inlineTemplate:{render:function(){"+r.render+"},staticRenderFns:["+r.staticRenderFns.map((function(t){return"function(){"+t+"}"})).join(",")+"]}"}}(t,e);i&&(n+=i+",")}return n=n.replace(/,$/,"")+"}",t.dynamicAttrs&&(n="_b("+n+',"'+t.tag+'",'+es(t.dynamicAttrs)+")"),t.wrapData&&(n=t.wrapData(n)),t.wrapListeners&&(n=t.wrapListeners(n)),n}function Xa(t){return 1===t.type&&("slot"===t.tag||t.children.some(Xa))}function Ga(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return Ja(t,e,Ga,"null");if(t.for&&!t.forProcessed)return Ya(t,e,Ga);var r="_empty_"===t.slotScope?"":String(t.slotScope),o="function("+r+"){return "+("template"===t.tag?t.if&&n?"("+t.if+")?"+(Za(t,e)||"undefined")+":undefined":Za(t,e)||"undefined":Wa(t,e))+"}",i=r?"":",proxy:true";return"{key:"+(t.slotTarget||'"default"')+",fn:"+o+i+"}"}function Za(t,e,n,r,o){var i=t.children;if(i.length){var a=i[0];if(1===i.length&&a.for&&"template"!==a.tag&&"slot"!==a.tag){var s=n?e.maybeComponent(a)?",1":",0":"";return""+(r||Wa)(a,e)+s}var c=n?function(t,e){for(var n=0,r=0;r':'

',ss.innerHTML.indexOf(" ")>0}var fs=!!V&&us(!1),ps=!!V&&us(!0),ds=y((function(t){var e=nr(t);return e&&e.innerHTML})),hs=Cn.prototype.$mount;Cn.prototype.$mount=function(t,e){if((t=t&&nr(t))===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=ds(r));else{if(!r.nodeType)return this;r=r.innerHTML}else t&&(r=function(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}(t));if(r){var o=ls(r,{outputSourceRange:!1,shouldDecodeNewlines:fs,shouldDecodeNewlinesForHref:ps,delimiters:n.delimiters,comments:n.comments},this),i=o.render,a=o.staticRenderFns;n.render=i,n.staticRenderFns=a}}return hs.call(this,t,e)},Cn.compile=ls;var vs=("undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).__VUE_DEVTOOLS_GLOBAL_HOOK__;function ms(t,e){if(void 0===e&&(e=[]),null===t||"object"!=typeof t)return t;var n,r=(n=function(e){return e.original===t},e.filter(n)[0]);if(r)return r.copy;var o=Array.isArray(t)?[]:{};return e.push({original:t,copy:o}),Object.keys(t).forEach((function(n){o[n]=ms(t[n],e)})),o}function gs(t,e){Object.keys(t).forEach((function(n){return e(t[n],n)}))}function ys(t){return null!==t&&"object"==typeof t}var bs=function(t,e){this.runtime=e,this._children=Object.create(null),this._rawModule=t;var n=t.state;this.state=("function"==typeof n?n():n)||{}},ws={namespaced:{configurable:!0}};ws.namespaced.get=function(){return!!this._rawModule.namespaced},bs.prototype.addChild=function(t,e){this._children[t]=e},bs.prototype.removeChild=function(t){delete this._children[t]},bs.prototype.getChild=function(t){return this._children[t]},bs.prototype.hasChild=function(t){return t in this._children},bs.prototype.update=function(t){this._rawModule.namespaced=t.namespaced,t.actions&&(this._rawModule.actions=t.actions),t.mutations&&(this._rawModule.mutations=t.mutations),t.getters&&(this._rawModule.getters=t.getters)},bs.prototype.forEachChild=function(t){gs(this._children,t)},bs.prototype.forEachGetter=function(t){this._rawModule.getters&&gs(this._rawModule.getters,t)},bs.prototype.forEachAction=function(t){this._rawModule.actions&&gs(this._rawModule.actions,t)},bs.prototype.forEachMutation=function(t){this._rawModule.mutations&&gs(this._rawModule.mutations,t)},Object.defineProperties(bs.prototype,ws);var xs,Ss=function(t){this.register([],t,!1)};function ks(t,e,n){if(e.update(n),n.modules)for(var r in n.modules){if(!e.getChild(r))return;ks(t.concat(r),e.getChild(r),n.modules[r])}}Ss.prototype.get=function(t){return t.reduce((function(t,e){return t.getChild(e)}),this.root)},Ss.prototype.getNamespace=function(t){var e=this.root;return t.reduce((function(t,n){return t+((e=e.getChild(n)).namespaced?n+"/":"")}),"")},Ss.prototype.update=function(t){ks([],this.root,t)},Ss.prototype.register=function(t,e,n){var r=this;void 0===n&&(n=!0);var o=new bs(e,n);0===t.length?this.root=o:this.get(t.slice(0,-1)).addChild(t[t.length-1],o);e.modules&&gs(e.modules,(function(e,o){r.register(t.concat(o),e,n)}))},Ss.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1],r=e.getChild(n);r&&r.runtime&&e.removeChild(n)},Ss.prototype.isRegistered=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];return!!e&&e.hasChild(n)};var _s=function(t){var e=this;void 0===t&&(t={}),!xs&&"undefined"!=typeof window&&window.Vue&&Es(window.Vue);var n=t.plugins;void 0===n&&(n=[]);var r=t.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new Ss(t),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new xs,this._makeLocalGettersCache=Object.create(null);var o=this,i=this.dispatch,a=this.commit;this.dispatch=function(t,e){return i.call(o,t,e)},this.commit=function(t,e,n){return a.call(o,t,e,n)},this.strict=r;var s=this._modules.root.state;Ts(this,s,[],this._modules.root),Ds(this,s),n.forEach((function(t){return t(e)})),(void 0!==t.devtools?t.devtools:xs.config.devtools)&&function(t){vs&&(t._devtoolHook=vs,vs.emit("vuex:init",t),vs.on("vuex:travel-to-state",(function(e){t.replaceState(e)})),t.subscribe((function(t,e){vs.emit("vuex:mutation",t,e)}),{prepend:!0}),t.subscribeAction((function(t,e){vs.emit("vuex:action",t,e)}),{prepend:!0}))}(this)},Os={state:{configurable:!0}};function Cs(t,e,n){return e.indexOf(t)<0&&(n&&n.prepend?e.unshift(t):e.push(t)),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function Ms(t,e){t._actions=Object.create(null),t._mutations=Object.create(null),t._wrappedGetters=Object.create(null),t._modulesNamespaceMap=Object.create(null);var n=t.state;Ts(t,n,[],t._modules.root,!0),Ds(t,n,e)}function Ds(t,e,n){var r=t._vm;t.getters={},t._makeLocalGettersCache=Object.create(null);var o=t._wrappedGetters,i={};gs(o,(function(e,n){i[n]=function(t,e){return function(){return t(e)}}(e,t),Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})}));var a=xs.config.silent;xs.config.silent=!0,t._vm=new xs({data:{$$state:e},computed:i}),xs.config.silent=a,t.strict&&function(t){t._vm.$watch((function(){return this._data.$$state}),(function(){}),{deep:!0,sync:!0})}(t),r&&(n&&t._withCommit((function(){r._data.$$state=null})),xs.nextTick((function(){return r.$destroy()})))}function Ts(t,e,n,r,o){var i=!n.length,a=t._modules.getNamespace(n);if(r.namespaced&&(t._modulesNamespaceMap[a],t._modulesNamespaceMap[a]=r),!i&&!o){var s=$s(e,n.slice(0,-1)),c=n[n.length-1];t._withCommit((function(){xs.set(s,c,r.state)}))}var l=r.context=function(t,e,n){var r=""===e,o={dispatch:r?t.dispatch:function(n,r,o){var i=As(n,r,o),a=i.payload,s=i.options,c=i.type;return s&&s.root||(c=e+c),t.dispatch(c,a)},commit:r?t.commit:function(n,r,o){var i=As(n,r,o),a=i.payload,s=i.options,c=i.type;s&&s.root||(c=e+c),t.commit(c,a,s)}};return Object.defineProperties(o,{getters:{get:r?function(){return t.getters}:function(){return function(t,e){if(!t._makeLocalGettersCache[e]){var n={},r=e.length;Object.keys(t.getters).forEach((function(o){if(o.slice(0,r)===e){var i=o.slice(r);Object.defineProperty(n,i,{get:function(){return t.getters[o]},enumerable:!0})}})),t._makeLocalGettersCache[e]=n}return t._makeLocalGettersCache[e]}(t,e)}},state:{get:function(){return $s(t.state,n)}}}),o}(t,a,n);r.forEachMutation((function(e,n){!function(t,e,n,r){(t._mutations[e]||(t._mutations[e]=[])).push((function(e){n.call(t,r.state,e)}))}(t,a+n,e,l)})),r.forEachAction((function(e,n){var r=e.root?n:a+n,o=e.handler||e;!function(t,e,n,r){(t._actions[e]||(t._actions[e]=[])).push((function(e){var o,i=n.call(t,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:t.getters,rootState:t.state},e);return(o=i)&&"function"==typeof o.then||(i=Promise.resolve(i)),t._devtoolHook?i.catch((function(e){throw t._devtoolHook.emit("vuex:error",e),e})):i}))}(t,r,o,l)})),r.forEachGetter((function(e,n){!function(t,e,n,r){if(t._wrappedGetters[e])return;t._wrappedGetters[e]=function(t){return n(r.state,r.getters,t.state,t.getters)}}(t,a+n,e,l)})),r.forEachChild((function(r,i){Ts(t,e,n.concat(i),r,o)}))}function $s(t,e){return e.reduce((function(t,e){return t[e]}),t)}function As(t,e,n){return ys(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function Es(t){xs&&t===xs|| +var t=Object.freeze({}),e=Array.isArray;function n(t){return null==t}function r(t){return null!=t}function i(t){return!0===t}function o(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function s(t){return"function"==typeof t}function a(t){return null!==t&&"object"==typeof t}var l=Object.prototype.toString;function c(t){return"[object Object]"===l.call(t)}function u(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function d(t){return r(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function f(t){return null==t?"":Array.isArray(t)||c(t)&&t.toString===l?JSON.stringify(t,null,2):String(t)}function h(t){var e=parseFloat(t);return isNaN(e)?t:e}function p(t,e){for(var n=Object.create(null),r=t.split(","),i=0;i-1)return t.splice(n,1)}}var y=Object.prototype.hasOwnProperty;function b(t,e){return y.call(t,e)}function w(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var x=/-(\w)/g,S=w((function(t){return t.replace(x,(function(t,e){return e?e.toUpperCase():""}))})),k=w((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),O=/\B([A-Z])/g,_=w((function(t){return t.replace(O,"-$1").toLowerCase()}));var M=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var r=arguments.length;return r?r>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function C(t,e){e=e||0;for(var n=t.length-e,r=new Array(n);n--;)r[n]=t[n+e];return r}function $(t,e){for(var n in e)t[n]=e[n];return t}function T(t){for(var e={},n=0;n0,Y=K&&K.indexOf("edge/")>0;K&&K.indexOf("android");var G=K&&/iphone|ipad|ipod|ios/.test(K);K&&/chrome\/\d+/.test(K),K&&/phantomjs/.test(K);var Z,X=K&&K.match(/firefox\/(\d+)/),Q={}.watch,tt=!1;if(J)try{var et={};Object.defineProperty(et,"passive",{get:function(){tt=!0}}),window.addEventListener("test-passive",null,et)}catch(_g){}var nt=function(){return void 0===Z&&(Z=!J&&"undefined"!=typeof global&&(global.process&&"server"===global.process.env.VUE_ENV)),Z},rt=J&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function it(t){return"function"==typeof t&&/native code/.test(t.toString())}var ot,st="undefined"!=typeof Symbol&&it(Symbol)&&"undefined"!=typeof Reflect&&it(Reflect.ownKeys);ot="undefined"!=typeof Set&&it(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var at=null;function lt(t){void 0===t&&(t=null),t||at&&at._scope.off(),at=t,t&&t._scope.on()}var ct=function(){function t(t,e,n,r,i,o,s,a){this.tag=t,this.data=e,this.children=n,this.text=r,this.elm=i,this.ns=void 0,this.context=o,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=s,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=a,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),ut=function(t){void 0===t&&(t="");var e=new ct;return e.text=t,e.isComment=!0,e};function dt(t){return new ct(void 0,void 0,void 0,String(t))}function ft(t){var e=new ct(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}var ht=0,pt=function(){function t(){this.id=ht++,this.subs=[]}return t.prototype.addSub=function(t){this.subs.push(t)},t.prototype.removeSub=function(t){v(this.subs,t)},t.prototype.depend=function(e){t.target&&t.target.addDep(this)},t.prototype.notify=function(t){for(var e=this.subs.slice(),n=0,r=e.length;n0&&(Bt((l=Vt(l,"".concat(s||"","_").concat(a)))[0])&&Bt(u)&&(d[c]=dt(u.text+l[0].text),l.shift()),d.push.apply(d,l)):o(l)?Bt(u)?d[c]=dt(u.text+l):""!==l&&d.push(dt(l)):Bt(l)&&Bt(u)?d[c]=dt(u.text+l.text):(i(t._isVList)&&r(l.tag)&&n(l.key)&&r(s)&&(l.key="__vlist".concat(s,"_").concat(a,"__")),d.push(l)));return d}function Wt(t,n,l,c,u,d){return(e(l)||o(l))&&(u=c,c=l,l=void 0),i(d)&&(u=2),function(t,n,i,o,l){if(r(i)&&r(i.__ob__))return ut();r(i)&&r(i.is)&&(n=i.is);if(!n)return ut();e(o)&&s(o[0])&&((i=i||{}).scopedSlots={default:o[0]},o.length=0);2===l?o=Lt(o):1===l&&(o=function(t){for(var n=0;n0,a=n?!!n.$stable:!s,l=n&&n.$key;if(n){if(n._normalized)return n._normalized;if(a&&i&&i!==t&&l===i.$key&&!s&&!i.$hasNormal)return i;for(var c in o={},n)n[c]&&"$"!==c[0]&&(o[c]=ue(e,r,c,n[c]))}else o={};for(var u in r)u in o||(o[u]=de(r,u));return n&&Object.isExtensible(n)&&(n._normalized=o),V(o,"$stable",a),V(o,"$key",l),V(o,"$hasNormal",s),o}function ue(t,n,r,i){var o=function(){var n=at;lt(t);var r=arguments.length?i.apply(null,arguments):i({}),o=(r=r&&"object"==typeof r&&!e(r)?[r]:Lt(r))&&r[0];return lt(n),r&&(!o||1===r.length&&o.isComment&&!le(o))?void 0:r};return i.proxy&&Object.defineProperty(n,r,{get:o,enumerable:!0,configurable:!0}),o}function de(t,e){return function(){return t[e]}}function fe(e){var n=e.$options,r=n.setup;if(r){var i=e._setupContext=function(e){return{get attrs(){if(!e._attrsProxy){var n=e._attrsProxy={};V(n,"_v_attr_proxy",!0),he(n,e.$attrs,t,e,"$attrs")}return e._attrsProxy},get listeners(){e._listenersProxy||he(e._listenersProxy={},e.$listeners,t,e,"$listeners");return e._listenersProxy},get slots(){return function(t){t._slotsProxy||me(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(e)},emit:M(e.$emit,e),expose:function(t){t&&Object.keys(t).forEach((function(n){return Pt(e,t,n)}))}}}(e);lt(e),gt();var o=qe(r,null,[e._props||Nt({}),i],e,"setup");if(vt(),lt(),s(o))n.render=o;else if(a(o))if(e._setupState=o,o.__sfc){var l=e._setupProxy={};for(var c in o)"__sfc"!==c&&Pt(l,o,c)}else for(var c in o)B(c)||Pt(e,o,c)}}function he(t,e,n,r,i){var o=!1;for(var s in e)s in t?e[s]!==n[s]&&(o=!0):(o=!0,pe(t,s,r,i));for(var s in t)s in e||(o=!0,delete t[s]);return o}function pe(t,e,n,r){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[r][e]}})}function me(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}var ge,ve=null;function ye(t,e){return(t.__esModule||st&&"Module"===t[Symbol.toStringTag])&&(t=t.default),a(t)?e.extend(t):t}function be(t){if(e(t))for(var n=0;ndocument.createEvent("Event").timeStamp&&(ze=function(){return je.now()})}var Fe,Le=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function Be(){var t,e;for(Re=ze(),Pe=!0,De.sort(Le),Ie=0;IeIe&&De[n].id>t.id;)n--;De.splice(n+1,0,t)}else De.push(t);Ee||(Ee=!0,nn(Be))}}(this)},t.prototype.run=function(){if(this.active){var t=this.get();if(t!==this.value||a(t)||this.deep){var e=this.value;if(this.value=t,this.user){var n='callback for watcher "'.concat(this.expression,'"');qe(this.cb,this.vm,[t,e],this.vm,n)}else this.cb.call(this.vm,t,e)}}},t.prototype.evaluate=function(){this.value=this.get(),this.dirty=!1},t.prototype.depend=function(){for(var t=this.deps.length;t--;)this.deps[t].depend()},t.prototype.teardown=function(){if(this.vm&&!this.vm._isBeingDestroyed&&v(this.vm._scope.effects,this),this.active){for(var t=this.deps.length;t--;)this.deps[t].removeSub(this);this.active=!1,this.onStop&&this.onStop()}},t}(),cn={enumerable:!0,configurable:!0,get:D,set:D};function un(t,e,n){cn.get=function(){return this[e][n]},cn.set=function(t){this[e][n]=t},Object.defineProperty(t,n,cn)}function dn(t){var n=t.$options;if(n.props&&function(t,e){var n=t.$options.propsData||{},r=t._props=Nt({}),i=t.$options._propKeys=[];t.$parent&&kt(!1);var o=function(o){i.push(o);var s=jn(o,e,n,t);Ct(r,o,s),o in t||un(t,"_props",o)};for(var s in e)o(s);kt(!0)}(t,n.props),fe(t),n.methods&&function(t,e){for(var n in t.$options.props,e)t[n]="function"!=typeof e[n]?D:M(e[n],t)}(t,n.methods),n.data)!function(t){var e=t.$options.data;c(e=t._data=s(e)?function(t,e){gt();try{return t.call(e,e)}catch(_g){return We(_g,e,"data()"),{}}finally{vt()}}(e,t):e||{})||(e={});var n=Object.keys(e),r=t.$options.props;t.$options.methods;var i=n.length;for(;i--;){var o=n[i];r&&b(r,o)||B(o)||un(t,"_data",o)}var a=Mt(e);a&&a.vmCount++}(t);else{var r=Mt(t._data={});r&&r.vmCount++}n.computed&&function(t,e){var n=t._computedWatchers=Object.create(null),r=nt();for(var i in e){var o=e[i],a=s(o)?o:o.get;r||(n[i]=new ln(t,a||D,D,fn)),i in t||hn(t,i,o)}}(t,n.computed),n.watch&&n.watch!==Q&&function(t,n){for(var r in n){var i=n[r];if(e(i))for(var o=0;o-1)if(o&&!b(i,"default"))a=!1;else if(""===a||a===_(t)){var c=Vn(String,i.type);(c<0||l-1:"string"==typeof t?t.split(",").indexOf(n)>-1:(r=t,"[object RegExp]"===l.call(r)&&t.test(n));var r}function Hn(t,e){var n=t.cache,r=t.keys,i=t._vnode;for(var o in n){var s=n[o];if(s){var a=s.name;a&&!e(a)&&Un(n,o,r,i)}}}function Un(t,e,n,r){var i=t[e];!i||r&&i.tag===r.tag||i.componentInstance.$destroy(),t[e]=null,v(n,e)}Wn.prototype._init=function(e){var n=this;n._uid=bn++,n._isVue=!0,n.__v_skip=!0,n._scope=new Ve(!0),n._scope._vm=!0,e&&e._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),r=e._parentVnode;n.parent=e.parent,n._parentVnode=r;var i=r.componentOptions;n.propsData=i.propsData,n._parentListeners=i.listeners,n._renderChildren=i.children,n._componentTag=i.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(n,e):n.$options=Rn(wn(n.constructor),e||{},n),n._renderProxy=n,n._self=n,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(n),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&ke(t,e)}(n),function(e){e._vnode=null,e._staticTrees=null;var n=e.$options,r=e.$vnode=n._parentVnode,i=r&&r.context;e.$slots=se(n._renderChildren,i),e.$scopedSlots=r?ce(e.$parent,r.data.scopedSlots,e.$slots):t,e._c=function(t,n,r,i){return Wt(e,t,n,r,i,!1)},e.$createElement=function(t,n,r,i){return Wt(e,t,n,r,i,!0)};var o=r&&r.data;Ct(e,"$attrs",o&&o.attrs||t,null,!0),Ct(e,"$listeners",n._parentListeners||t,null,!0)}(n),Te(n,"beforeCreate",void 0,!1),function(t){var e=yn(t.$options.inject,t);e&&(kt(!1),Object.keys(e).forEach((function(n){Ct(t,n,e[n])})),kt(!0))}(n),dn(n),vn(n),Te(n,"created"),n.$options.el&&n.$mount(n.$options.el)},function(t){var e={get:function(){return this._data}},n={get:function(){return this._props}};Object.defineProperty(t.prototype,"$data",e),Object.defineProperty(t.prototype,"$props",n),t.prototype.$set=$t,t.prototype.$delete=Tt,t.prototype.$watch=function(t,e,n){var r=this;if(c(e))return gn(r,t,e,n);(n=n||{}).user=!0;var i=new ln(r,t,e,n);if(n.immediate){var o='callback for immediate watcher "'.concat(i.expression,'"');gt(),qe(e,r,[i.value],r,o),vt()}return function(){i.teardown()}}}(Wn),function(t){var n=/^hook:/;t.prototype.$on=function(t,r){var i=this;if(e(t))for(var o=0,s=t.length;o1?C(n):n;for(var r=C(arguments,1),i='event handler for "'.concat(t,'"'),o=0,s=n.length;oparseInt(this.max)&&Un(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)Un(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){Hn(t,(function(t){return Kn(e,t)}))})),this.$watch("exclude",(function(e){Hn(t,(function(t){return!Kn(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=be(t),n=e&&e.componentOptions;if(n){var r=Jn(n),i=this.include,o=this.exclude;if(i&&(!r||!Kn(i,r))||o&&r&&Kn(o,r))return e;var s=this.cache,a=this.keys,l=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;s[l]?(e.componentInstance=s[l].componentInstance,v(a,l),a.push(l)):(this.vnodeToCache=e,this.keyToCache=l),e.data.keepAlive=!0}return e||t&&t[0]}}};!function(t){var e={get:function(){return F}};Object.defineProperty(t,"config",e),t.util={warn:Tn,extend:$,mergeOptions:Rn,defineReactive:Ct},t.set=$t,t.delete=Tt,t.nextTick=nn,t.observable=function(t){return Mt(t),t},t.options=Object.create(null),z.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,$(t.options.components,Gn),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=C(arguments,1);return n.unshift(this),s(t.install)?t.install.apply(t,n):s(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Rn(this.options,t),this}}(t),qn(t),function(t){z.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&c(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&s(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(Wn),Object.defineProperty(Wn.prototype,"$isServer",{get:nt}),Object.defineProperty(Wn.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(Wn,"FunctionalRenderContext",{value:xn}),Wn.version="2.7.10";var Zn=p("style,class"),Xn=p("input,textarea,option,select,progress"),Qn=function(t,e,n){return"value"===n&&Xn(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},tr=p("contenteditable,draggable,spellcheck"),er=p("events,caret,typing,plaintext-only"),nr=p("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),rr="http://www.w3.org/1999/xlink",ir=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},or=function(t){return ir(t)?t.slice(6,t.length):""},sr=function(t){return null==t||!1===t};function ar(t){for(var e=t.data,n=t,i=t;r(i.componentInstance);)(i=i.componentInstance._vnode)&&i.data&&(e=lr(i.data,e));for(;r(n=n.parent);)n&&n.data&&(e=lr(e,n.data));return function(t,e){if(r(t)||r(e))return cr(t,ur(e));return""}(e.staticClass,e.class)}function lr(t,e){return{staticClass:cr(t.staticClass,e.staticClass),class:r(t.class)?[t.class,e.class]:e.class}}function cr(t,e){return t?e?t+" "+e:t:e||""}function ur(t){return Array.isArray(t)?function(t){for(var e,n="",i=0,o=t.length;i-1?Rr(t,e,n):nr(e)?sr(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):tr(e)?t.setAttribute(e,function(t,e){return sr(e)||"false"===e?"false":"contenteditable"===t&&er(e)?e:"true"}(e,n)):ir(e)?sr(n)?t.removeAttributeNS(rr,or(e)):t.setAttributeNS(rr,e,n):Rr(t,e,n)}function Rr(t,e,n){if(sr(n))t.removeAttribute(e);else{if(H&&!U&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var r=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",r)};t.addEventListener("input",r),t.__ieph=!0}t.setAttribute(e,n)}}var zr={create:Pr,update:Pr};function jr(t,e){var i=e.elm,o=e.data,s=t.data;if(!(n(o.staticClass)&&n(o.class)&&(n(s)||n(s.staticClass)&&n(s.class)))){var a=ar(e),l=i._transitionClasses;r(l)&&(a=cr(a,ur(l))),a!==i._prevClass&&(i.setAttribute("class",a),i._prevClass=a)}}var Fr,Lr,Br,Vr,Wr,qr,Jr={create:jr,update:jr},Kr=/[\w).+\-_$\]]/;function Hr(t){var e,n,r,i,o,s=!1,a=!1,l=!1,c=!1,u=0,d=0,f=0,h=0;for(r=0;r=0&&" "===(m=t.charAt(p));p--);m&&Kr.test(m)||(c=!0)}}else void 0===i?(h=r+1,i=t.slice(0,r).trim()):g();function g(){(o||(o=[])).push(t.slice(h,r).trim()),h=r+1}if(void 0===i?i=t.slice(0,r).trim():0!==h&&g(),o)for(r=0;r-1?{exp:t.slice(0,Vr),key:'"'+t.slice(Vr+1)+'"'}:{exp:t,key:null};Lr=t,Vr=Wr=qr=0;for(;!ui();)di(Br=ci())?hi(Br):91===Br&&fi(Br);return{exp:t.slice(0,Wr),key:t.slice(Wr+1,qr)}}(t);return null===n.key?"".concat(t,"=").concat(e):"$set(".concat(n.exp,", ").concat(n.key,", ").concat(e,")")}function ci(){return Lr.charCodeAt(++Vr)}function ui(){return Vr>=Fr}function di(t){return 34===t||39===t}function fi(t){var e=1;for(Wr=Vr;!ui();)if(di(t=ci()))hi(t);else if(91===t&&e++,93===t&&e--,0===e){qr=Vr;break}}function hi(t){for(var e=t;!ui()&&(t=ci())!==e;);}var pi;function mi(t,e,n){var r=pi;return function i(){var o=e.apply(null,arguments);null!==o&&yi(t,i,n,r)}}var gi=Ue&&!(X&&Number(X[1])<=53);function vi(t,e,n,r){if(gi){var i=Re,o=e;e=o._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=i||t.timeStamp<=0||t.target.ownerDocument!==document)return o.apply(this,arguments)}}pi.addEventListener(t,e,tt?{capture:n,passive:r}:n)}function yi(t,e,n,r){(r||pi).removeEventListener(t,e._wrapper||e,n)}function bi(t,e){if(!n(t.data.on)||!n(e.data.on)){var i=e.data.on||{},o=t.data.on||{};pi=e.elm||t.elm,function(t){if(r(t.__r)){var e=H?"change":"input";t[e]=[].concat(t.__r,t[e]||[]),delete t.__r}r(t.__c)&&(t.change=[].concat(t.__c,t.change||[]),delete t.__c)}(i),zt(i,o,vi,yi,mi,e.context),pi=void 0}}var wi,xi={create:bi,update:bi,destroy:function(t){return bi(t,kr)}};function Si(t,e){if(!n(t.data.domProps)||!n(e.data.domProps)){var o,s,a=e.elm,l=t.data.domProps||{},c=e.data.domProps||{};for(o in(r(c.__ob__)||i(c._v_attr_proxy))&&(c=e.data.domProps=$({},c)),l)o in c||(a[o]="");for(o in c){if(s=c[o],"textContent"===o||"innerHTML"===o){if(e.children&&(e.children.length=0),s===l[o])continue;1===a.childNodes.length&&a.removeChild(a.childNodes[0])}if("value"===o&&"PROGRESS"!==a.tagName){a._value=s;var u=n(s)?"":String(s);ki(a,u)&&(a.value=u)}else if("innerHTML"===o&&hr(a.tagName)&&n(a.innerHTML)){(wi=wi||document.createElement("div")).innerHTML="".concat(s,"");for(var d=wi.firstChild;a.firstChild;)a.removeChild(a.firstChild);for(;d.firstChild;)a.appendChild(d.firstChild)}else if(s!==l[o])try{a[o]=s}catch(_g){}}}}function ki(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(_g){}return n&&t.value!==e}(t,e)||function(t,e){var n=t.value,i=t._vModifiers;if(r(i)){if(i.number)return h(n)!==h(e);if(i.trim)return n.trim()!==e.trim()}return n!==e}(t,e))}var Oi={create:Si,update:Si},_i=w((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var r=t.split(n);r.length>1&&(e[r[0].trim()]=r[1].trim())}})),e}));function Mi(t){var e=Ci(t.style);return t.staticStyle?$(t.staticStyle,e):e}function Ci(t){return Array.isArray(t)?T(t):"string"==typeof t?_i(t):t}var $i,Ti=/^--/,Di=/\s*!important$/,Ni=function(t,e,n){if(Ti.test(e))t.style.setProperty(e,n);else if(Di.test(n))t.style.setProperty(_(e),n.replace(Di,""),"important");else{var r=Ei(e);if(Array.isArray(n))for(var i=0,o=n.length;i-1?e.split(Ri).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" ".concat(t.getAttribute("class")||""," ");n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function ji(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(Ri).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" ".concat(t.getAttribute("class")||""," "),r=" "+e+" ";n.indexOf(r)>=0;)n=n.replace(r," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function Fi(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&$(e,Li(t.name||"v")),$(e,t),e}return"string"==typeof t?Li(t):void 0}}var Li=w((function(t){return{enterClass:"".concat(t,"-enter"),enterToClass:"".concat(t,"-enter-to"),enterActiveClass:"".concat(t,"-enter-active"),leaveClass:"".concat(t,"-leave"),leaveToClass:"".concat(t,"-leave-to"),leaveActiveClass:"".concat(t,"-leave-active")}})),Bi=J&&!U,Vi="transition",Wi="transitionend",qi="animation",Ji="animationend";Bi&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(Vi="WebkitTransition",Wi="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(qi="WebkitAnimation",Ji="webkitAnimationEnd"));var Ki=J?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function Hi(t){Ki((function(){Ki(t)}))}function Ui(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),zi(t,e))}function Yi(t,e){t._transitionClasses&&v(t._transitionClasses,e),ji(t,e)}function Gi(t,e,n){var r=Xi(t,e),i=r.type,o=r.timeout,s=r.propCount;if(!i)return n();var a="transition"===i?Wi:Ji,l=0,c=function(){t.removeEventListener(a,u),n()},u=function(e){e.target===t&&++l>=s&&c()};setTimeout((function(){l0&&(n="transition",u=s,d=o.length):"animation"===e?c>0&&(n="animation",u=c,d=l.length):d=(n=(u=Math.max(s,c))>0?s>c?"transition":"animation":null)?"transition"===n?o.length:l.length:0,{type:n,timeout:u,propCount:d,hasTransform:"transition"===n&&Zi.test(r[Vi+"Property"])}}function Qi(t,e){for(;t.length1}function oo(t,e){!0!==e.data.show&&eo(e)}var so=function(t){var s,a,l={},c=t.modules,u=t.nodeOps;for(s=0;sp?w(t,n(i[v+1])?null:i[v+1].elm,i,h,v,o):h>v&&S(e,d,p)}(d,m,g,o,c):r(g)?(r(t.text)&&u.setTextContent(d,""),w(d,null,g,0,g.length-1,o)):r(m)?S(m,0,m.length-1):r(t.text)&&u.setTextContent(d,""):t.text!==e.text&&u.setTextContent(d,e.text),r(p)&&r(h=p.hook)&&r(h=h.postpatch)&&h(t,e)}}}function M(t,e,n){if(i(n)&&r(t.parent))t.parent.data.pendingInsert=e;else for(var o=0;o-1,s.selected!==o&&(s.selected=o);else if(E(fo(s),r))return void(t.selectedIndex!==a&&(t.selectedIndex=a));i||(t.selectedIndex=-1)}}function uo(t,e){return e.every((function(e){return!E(e,t)}))}function fo(t){return"_value"in t?t._value:t.value}function ho(t){t.target.composing=!0}function po(t){t.target.composing&&(t.target.composing=!1,mo(t.target,"input"))}function mo(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function go(t){return!t.componentInstance||t.data&&t.data.transition?t:go(t.componentInstance._vnode)}var vo={model:ao,show:{bind:function(t,e,n){var r=e.value,i=(n=go(n)).data&&n.data.transition,o=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;r&&i?(n.data.show=!0,eo(n,(function(){t.style.display=o}))):t.style.display=r?o:"none"},update:function(t,e,n){var r=e.value;!r!=!e.oldValue&&((n=go(n)).data&&n.data.transition?(n.data.show=!0,r?eo(n,(function(){t.style.display=t.__vOriginalDisplay})):no(n,(function(){t.style.display="none"}))):t.style.display=r?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,r,i){i||(t.style.display=t.__vOriginalDisplay)}}},yo={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function bo(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?bo(be(e.children)):t}function wo(t){var e={},n=t.$options;for(var r in n.propsData)e[r]=t[r];var i=n._parentListeners;for(var r in i)e[S(r)]=i[r];return e}function xo(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var So=function(t){return t.tag||le(t)},ko=function(t){return"show"===t.name},Oo={name:"transition",props:yo,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(So)).length){var r=this.mode,i=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return i;var s=bo(i);if(!s)return i;if(this._leaving)return xo(t,i);var a="__transition-".concat(this._uid,"-");s.key=null==s.key?s.isComment?a+"comment":a+s.tag:o(s.key)?0===String(s.key).indexOf(a)?s.key:a+s.key:s.key;var l=(s.data||(s.data={})).transition=wo(this),c=this._vnode,u=bo(c);if(s.data.directives&&s.data.directives.some(ko)&&(s.data.show=!0),u&&u.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(s,u)&&!le(u)&&(!u.componentInstance||!u.componentInstance._vnode.isComment)){var d=u.data.transition=$({},l);if("out-in"===r)return this._leaving=!0,jt(d,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),xo(t,i);if("in-out"===r){if(le(s))return c;var f,h=function(){f()};jt(l,"afterEnter",h),jt(l,"enterCancelled",h),jt(d,"delayLeave",(function(t){f=t}))}}return i}}},_o=$({tag:String,moveClass:String},yo);delete _o.mode;var Mo={props:_o,beforeMount:function(){var t=this,e=this._update;this._update=function(n,r){var i=_e(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,i(),e.call(t,n,r)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),r=this.prevChildren=this.children,i=this.$slots.default||[],o=this.children=[],s=wo(this),a=0;a-1?gr[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:gr[t]=/HTMLUnknownElement/.test(e.toString())},$(Wn.options.directives,vo),$(Wn.options.components,Do),Wn.prototype.__patch__=J?so:D,Wn.prototype.$mount=function(t,e){return function(t,e,n){var r;t.$el=e,t.$options.render||(t.$options.render=ut),Te(t,"beforeMount"),r=function(){t._update(t._render(),n)},new ln(t,r,D,{before:function(){t._isMounted&&!t._isDestroyed&&Te(t,"beforeUpdate")}},!0),n=!1;var i=t._preWatchers;if(i)for(var o=0;o\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Vo=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,Wo="[a-zA-Z_][\\-\\.0-9_a-zA-Z".concat(L.source,"]*"),qo="((?:".concat(Wo,"\\:)?").concat(Wo,")"),Jo=new RegExp("^<".concat(qo)),Ko=/^\s*(\/?)>/,Ho=new RegExp("^<\\/".concat(qo,"[^>]*>")),Uo=/^]+>/i,Yo=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},ts=/&(?:lt|gt|quot|amp|#39);/g,es=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,ns=p("pre,textarea",!0),rs=function(t,e){return t&&ns(t)&&"\n"===e[0]};function is(t,e){var n=e?es:ts;return t.replace(n,(function(t){return Qo[t]}))}function ss(t,e){for(var n,r,i=[],o=e.expectHTML,s=e.isUnaryTag||N,a=e.canBeLeftOpenTag||N,l=0,c=function(){if(n=t,r&&Zo(r)){var c=0,f=r.toLowerCase(),h=Xo[f]||(Xo[f]=new RegExp("([\\s\\S]*?)(]*>)","i"));S=t.replace(h,(function(t,n,r){return c=r.length,Zo(f)||"noscript"===f||(n=n.replace(//g,"$1").replace(//g,"$1")),rs(f,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));l+=t.length-S.length,t=S,d(f,l-c,l)}else{var p=t.indexOf("<");if(0===p){if(Yo.test(t)){var m=t.indexOf("--\x3e");if(m>=0)return e.shouldKeepComment&&e.comment&&e.comment(t.substring(4,m),l,l+m+3),u(m+3),"continue"}if(Go.test(t)){var g=t.indexOf("]>");if(g>=0)return u(g+2),"continue"}var v=t.match(Uo);if(v)return u(v[0].length),"continue";var y=t.match(Ho);if(y){var b=l;return u(y[0].length),d(y[1],b,l),"continue"}var w=function(){var e=t.match(Jo);if(e){var n={tagName:e[1],attrs:[],start:l};u(e[0].length);for(var r=void 0,i=void 0;!(r=t.match(Ko))&&(i=t.match(Vo)||t.match(Bo));)i.start=l,u(i[0].length),i.end=l,n.attrs.push(i);if(r)return n.unarySlash=r[1],u(r[0].length),n.end=l,n}}();if(w)return function(t){var n=t.tagName,l=t.unarySlash;o&&("p"===r&&Lo(n)&&d(r),a(n)&&r===n&&d(n));for(var c=s(n)||!!l,u=t.attrs.length,f=new Array(u),h=0;h=0){for(S=t.slice(p);!(Ho.test(S)||Jo.test(S)||Yo.test(S)||Go.test(S)||(k=S.indexOf("<",1))<0);)p+=k,S=t.slice(p);x=t.substring(0,p)}p<0&&(x=t),x&&u(x.length),e.chars&&x&&e.chars(x,l-x.length,l)}if(t===n)return e.chars&&e.chars(t),"break"};t;){if("break"===c())break}function u(e){l+=e,t=t.substring(e)}function d(t,n,o){var s,a;if(null==n&&(n=l),null==o&&(o=l),t)for(a=t.toLowerCase(),s=i.length-1;s>=0&&i[s].lowerCasedTag!==a;s--);else s=0;if(s>=0){for(var c=i.length-1;c>=s;c--)e.end&&e.end(i[c].tag,n,o);i.length=s,r=s&&i[s-1].tag}else"br"===a?e.start&&e.start(t,[],!0,n,o):"p"===a&&(e.start&&e.start(t,[],!1,n,o),e.end&&e.end(t,n,o))}d()}var as,ls,cs,us,ds,fs,hs,ps,ms=/^@|^v-on:/,gs=/^v-|^@|^:|^#/,vs=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,ys=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,bs=/^\(|\)$/g,ws=/^\[.*\]$/,xs=/:(.*)$/,Ss=/^:|^\.|^v-bind:/,ks=/\.[^.\]]+(?=[^\]]*$)/g,Os=/^v-slot(:|$)|^#/,_s=/[\r\n]/,Ms=/[ \f\t\r\n]+/g,Cs=w(zo);function $s(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:Is(e),rawAttrsMap:{},parent:n,children:[]}}function Ts(t,e){as=e.warn||Yr,fs=e.isPreTag||N,hs=e.mustUseProp||N,ps=e.getTagNamespace||N,e.isReservedTag,cs=Gr(e.modules,"transformNode"),us=Gr(e.modules,"preTransformNode"),ds=Gr(e.modules,"postTransformNode"),ls=e.delimiters;var n,r,i=[],o=!1!==e.preserveWhitespace,s=e.whitespace,a=!1,l=!1;function c(t){if(u(t),a||t.processed||(t=Ds(t,e)),i.length||t===n||n.if&&(t.elseif||t.else)&&As(n,{exp:t.elseif,block:t}),r&&!t.forbidden)if(t.elseif||t.else)s=t,c=function(t){for(var e=t.length;e--;){if(1===t[e].type)return t[e];t.pop()}}(r.children),c&&c.if&&As(c,{exp:s.elseif,block:s});else{if(t.slotScope){var o=t.slotTarget||'"default"';(r.scopedSlots||(r.scopedSlots={}))[o]=t}r.children.push(t),t.parent=r}var s,c;t.children=t.children.filter((function(t){return!t.slotScope})),u(t),t.pre&&(a=!1),fs(t.tag)&&(l=!1);for(var d=0;dl&&(a.push(o=t.slice(l,i)),s.push(JSON.stringify(o)));var c=Hr(r[1].trim());s.push("_s(".concat(c,")")),a.push({"@binding":c}),l=i+r[0].length}return l-1")+("true"===o?":(".concat(e,")"):":_q(".concat(e,",").concat(o,")"))),ni(t,"change","var $$a=".concat(e,",")+"$$el=$event.target,"+"$$c=$$el.checked?(".concat(o,"):(").concat(s,");")+"if(Array.isArray($$a)){"+"var $$v=".concat(r?"_n("+i+")":i,",")+"$$i=_i($$a,$$v);"+"if($$el.checked){$$i<0&&(".concat(li(e,"$$a.concat([$$v])"),")}")+"else{$$i>-1&&(".concat(li(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))"),")}")+"}else{".concat(li(e,"$$c"),"}"),null,!0)}(t,r,i);else if("input"===o&&"radio"===s)!function(t,e,n){var r=n&&n.number,i=ri(t,"value")||"null";i=r?"_n(".concat(i,")"):i,Zr(t,"checked","_q(".concat(e,",").concat(i,")")),ni(t,"change",li(e,i),null,!0)}(t,r,i);else if("input"===o||"textarea"===o)!function(t,e,n){var r=t.attrsMap.type,i=n||{},o=i.lazy,s=i.number,a=i.trim,l=!o&&"range"!==r,c=o?"change":"range"===r?"__r":"input",u="$event.target.value";a&&(u="$event.target.value.trim()");s&&(u="_n(".concat(u,")"));var d=li(e,u);l&&(d="if($event.target.composing)return;".concat(d));Zr(t,"value","(".concat(e,")")),ni(t,c,d,null,!0),(a||s)&&ni(t,"blur","$forceUpdate()")}(t,r,i);else if(!F.isReservedTag(o))return ai(t,r,i),!1;return!0},text:function(t,e){e.value&&Zr(t,"textContent","_s(".concat(e.value,")"),e)},html:function(t,e){e.value&&Zr(t,"innerHTML","_s(".concat(e.value,")"),e)}},qs={expectHTML:!0,modules:Fs,directives:Ws,isPreTag:function(t){return"pre"===t},isUnaryTag:jo,mustUseProp:Qn,canBeLeftOpenTag:Fo,isReservedTag:pr,getTagNamespace:mr,staticKeys:(Ls=Fs,Ls.reduce((function(t,e){return t.concat(e.staticKeys||[])}),[]).join(","))},Js=w((function(t){return p("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(t?","+t:""))}));function Ks(t,e){t&&(Bs=Js(e.staticKeys||""),Vs=e.isReservedTag||N,Hs(t),Us(t,!1))}function Hs(t){if(t.static=function(t){if(2===t.type)return!1;if(3===t.type)return!0;return!(!t.pre&&(t.hasBindings||t.if||t.for||m(t.tag)||!Vs(t.tag)||function(t){for(;t.parent;){if("template"!==(t=t.parent).tag)return!1;if(t.for)return!0}return!1}(t)||!Object.keys(t).every(Bs)))}(t),1===t.type){if(!Vs(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var e=0,n=t.children.length;e|^function(?:\s+[\w$]+)?\s*\(/,Gs=/\([^)]*?\);*$/,Zs=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,Xs={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},Qs={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},ta=function(t){return"if(".concat(t,")return null;")},ea={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:ta("$event.target !== $event.currentTarget"),ctrl:ta("!$event.ctrlKey"),shift:ta("!$event.shiftKey"),alt:ta("!$event.altKey"),meta:ta("!$event.metaKey"),left:ta("'button' in $event && $event.button !== 0"),middle:ta("'button' in $event && $event.button !== 1"),right:ta("'button' in $event && $event.button !== 2")};function na(t,e){var n=e?"nativeOn:":"on:",r="",i="";for(var o in t){var s=ra(t[o]);t[o]&&t[o].dynamic?i+="".concat(o,",").concat(s,","):r+='"'.concat(o,'":').concat(s,",")}return r="{".concat(r.slice(0,-1),"}"),i?n+"_d(".concat(r,",[").concat(i.slice(0,-1),"])"):n+r}function ra(t){if(!t)return"function(){}";if(Array.isArray(t))return"[".concat(t.map((function(t){return ra(t)})).join(","),"]");var e=Zs.test(t.value),n=Ys.test(t.value),r=Zs.test(t.value.replace(Gs,""));if(t.modifiers){var i="",o="",s=[],a=function(e){if(ea[e])o+=ea[e],Xs[e]&&s.push(e);else if("exact"===e){var n=t.modifiers;o+=ta(["ctrl","shift","alt","meta"].filter((function(t){return!n[t]})).map((function(t){return"$event.".concat(t,"Key")})).join("||"))}else s.push(e)};for(var l in t.modifiers)a(l);s.length&&(i+=function(t){return"if(!$event.type.indexOf('key')&&"+"".concat(t.map(ia).join("&&"),")return null;")}(s)),o&&(i+=o);var c=e?"return ".concat(t.value,".apply(null, arguments)"):n?"return (".concat(t.value,").apply(null, arguments)"):r?"return ".concat(t.value):t.value;return"function($event){".concat(i).concat(c,"}")}return e||n?t.value:"function($event){".concat(r?"return ".concat(t.value):t.value,"}")}function ia(t){var e=parseInt(t,10);if(e)return"$event.keyCode!==".concat(e);var n=Xs[t],r=Qs[t];return"_k($event.keyCode,"+"".concat(JSON.stringify(t),",")+"".concat(JSON.stringify(n),",")+"$event.key,"+"".concat(JSON.stringify(r))+")"}var oa={on:function(t,e){t.wrapListeners=function(t){return"_g(".concat(t,",").concat(e.value,")")}},bind:function(t,e){t.wrapData=function(n){return"_b(".concat(n,",'").concat(t.tag,"',").concat(e.value,",").concat(e.modifiers&&e.modifiers.prop?"true":"false").concat(e.modifiers&&e.modifiers.sync?",true":"",")")}},cloak:D},sa=function(t){this.options=t,this.warn=t.warn||Yr,this.transforms=Gr(t.modules,"transformCode"),this.dataGenFns=Gr(t.modules,"genData"),this.directives=$($({},oa),t.directives);var e=t.isReservedTag||N;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function aa(t,e){var n=new sa(e),r=t?"script"===t.tag?"null":la(t,n):'_c("div")';return{render:"with(this){return ".concat(r,"}"),staticRenderFns:n.staticRenderFns}}function la(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return ca(t,e);if(t.once&&!t.onceProcessed)return ua(t,e);if(t.for&&!t.forProcessed)return ha(t,e);if(t.if&&!t.ifProcessed)return da(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return function(t,e){var n=t.slotName||'"default"',r=va(t,e),i="_t(".concat(n).concat(r?",function(){return ".concat(r,"}"):""),o=t.attrs||t.dynamicAttrs?wa((t.attrs||[]).concat(t.dynamicAttrs||[]).map((function(t){return{name:S(t.name),value:t.value,dynamic:t.dynamic}}))):null,s=t.attrsMap["v-bind"];!o&&!s||r||(i+=",null");o&&(i+=",".concat(o));s&&(i+="".concat(o?"":",null",",").concat(s));return i+")"}(t,e);var n=void 0;if(t.component)n=function(t,e,n){var r=e.inlineTemplate?null:va(e,n,!0);return"_c(".concat(t,",").concat(pa(e,n)).concat(r?",".concat(r):"",")")}(t.component,t,e);else{var r=void 0,i=e.maybeComponent(t);(!t.plain||t.pre&&i)&&(r=pa(t,e));var o=void 0,s=e.options.bindings;i&&s&&!1!==s.__isScriptSetup&&(o=function(t,e){var n=S(e),r=k(n),i=function(i){return t[e]===i?e:t[n]===i?n:t[r]===i?r:void 0},o=i("setup-const")||i("setup-reactive-const");if(o)return o;var s=i("setup-let")||i("setup-ref")||i("setup-maybe-ref");if(s)return s}(s,t.tag)),o||(o="'".concat(t.tag,"'"));var a=t.inlineTemplate?null:va(t,e,!0);n="_c(".concat(o).concat(r?",".concat(r):"").concat(a?",".concat(a):"",")")}for(var l=0;l>>0}(s)):"",")")}(t,t.scopedSlots,e),",")),t.model&&(n+="model:{value:".concat(t.model.value,",callback:").concat(t.model.callback,",expression:").concat(t.model.expression,"},")),t.inlineTemplate){var o=function(t,e){var n=t.children[0];if(n&&1===n.type){var r=aa(n,e.options);return"inlineTemplate:{render:function(){".concat(r.render,"},staticRenderFns:[").concat(r.staticRenderFns.map((function(t){return"function(){".concat(t,"}")})).join(","),"]}")}}(t,e);o&&(n+="".concat(o,","))}return n=n.replace(/,$/,"")+"}",t.dynamicAttrs&&(n="_b(".concat(n,',"').concat(t.tag,'",').concat(wa(t.dynamicAttrs),")")),t.wrapData&&(n=t.wrapData(n)),t.wrapListeners&&(n=t.wrapListeners(n)),n}function ma(t){return 1===t.type&&("slot"===t.tag||t.children.some(ma))}function ga(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return da(t,e,ga,"null");if(t.for&&!t.forProcessed)return ha(t,e,ga);var r="_empty_"===t.slotScope?"":String(t.slotScope),i="function(".concat(r,"){")+"return ".concat("template"===t.tag?t.if&&n?"(".concat(t.if,")?").concat(va(t,e)||"undefined",":undefined"):va(t,e)||"undefined":la(t,e),"}"),o=r?"":",proxy:true";return"{key:".concat(t.slotTarget||'"default"',",fn:").concat(i).concat(o,"}")}function va(t,e,n,r,i){var o=t.children;if(o.length){var s=o[0];if(1===o.length&&s.for&&"template"!==s.tag&&"slot"!==s.tag){var a=n?e.maybeComponent(s)?",1":",0":"";return"".concat((r||la)(s,e)).concat(a)}var l=n?function(t,e){for(var n=0,r=0;r':'
',_a.innerHTML.indexOf(" ")>0}var Ta=!!J&&$a(!1),Da=!!J&&$a(!0),Na=w((function(t){var e=yr(t);return e&&e.innerHTML})),Aa=Wn.prototype.$mount;Wn.prototype.$mount=function(t,e){if((t=t&&yr(t))===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var r=n.template;if(r)if("string"==typeof r)"#"===r.charAt(0)&&(r=Na(r));else{if(!r.nodeType)return this;r=r.innerHTML}else t&&(r=function(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}(t));if(r){var i=Ca(r,{outputSourceRange:!1,shouldDecodeNewlines:Ta,shouldDecodeNewlinesForHref:Da,delimiters:n.delimiters,comments:n.comments},this),o=i.render,s=i.staticRenderFns;n.render=o,n.staticRenderFns=s}}return Aa.call(this,t,e)},Wn.compile=Ca;var Ea=("undefined"!=typeof window?window:"undefined"!=typeof global?global:{}).__VUE_DEVTOOLS_GLOBAL_HOOK__;function Pa(t,e){if(void 0===e&&(e=[]),null===t||"object"!=typeof t)return t;var n,r=(n=function(e){return e.original===t},e.filter(n)[0]);if(r)return r.copy;var i=Array.isArray(t)?[]:{};return e.push({original:t,copy:i}),Object.keys(t).forEach((function(n){i[n]=Pa(t[n],e)})),i}function Ia(t,e){Object.keys(t).forEach((function(n){return e(t[n],n)}))}function Ra(t){return null!==t&&"object"==typeof t}var za=function(t,e){this.runtime=e,this._children=Object.create(null),this._rawModule=t;var n=t.state;this.state=("function"==typeof n?n():n)||{}},ja={namespaced:{configurable:!0}};ja.namespaced.get=function(){return!!this._rawModule.namespaced},za.prototype.addChild=function(t,e){this._children[t]=e},za.prototype.removeChild=function(t){delete this._children[t]},za.prototype.getChild=function(t){return this._children[t]},za.prototype.hasChild=function(t){return t in this._children},za.prototype.update=function(t){this._rawModule.namespaced=t.namespaced,t.actions&&(this._rawModule.actions=t.actions),t.mutations&&(this._rawModule.mutations=t.mutations),t.getters&&(this._rawModule.getters=t.getters)},za.prototype.forEachChild=function(t){Ia(this._children,t)},za.prototype.forEachGetter=function(t){this._rawModule.getters&&Ia(this._rawModule.getters,t)},za.prototype.forEachAction=function(t){this._rawModule.actions&&Ia(this._rawModule.actions,t)},za.prototype.forEachMutation=function(t){this._rawModule.mutations&&Ia(this._rawModule.mutations,t)},Object.defineProperties(za.prototype,ja);var Fa,La=function(t){this.register([],t,!1)};function Ba(t,e,n){if(e.update(n),n.modules)for(var r in n.modules){if(!e.getChild(r))return;Ba(t.concat(r),e.getChild(r),n.modules[r])}}La.prototype.get=function(t){return t.reduce((function(t,e){return t.getChild(e)}),this.root)},La.prototype.getNamespace=function(t){var e=this.root;return t.reduce((function(t,n){return t+((e=e.getChild(n)).namespaced?n+"/":"")}),"")},La.prototype.update=function(t){Ba([],this.root,t)},La.prototype.register=function(t,e,n){var r=this;void 0===n&&(n=!0);var i=new za(e,n);0===t.length?this.root=i:this.get(t.slice(0,-1)).addChild(t[t.length-1],i);e.modules&&Ia(e.modules,(function(e,i){r.register(t.concat(i),e,n)}))},La.prototype.unregister=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1],r=e.getChild(n);r&&r.runtime&&e.removeChild(n)},La.prototype.isRegistered=function(t){var e=this.get(t.slice(0,-1)),n=t[t.length-1];return!!e&&e.hasChild(n)};var Va=function(t){var e=this;void 0===t&&(t={}),!Fa&&"undefined"!=typeof window&&window.Vue&&Ga(window.Vue);var n=t.plugins;void 0===n&&(n=[]);var r=t.strict;void 0===r&&(r=!1),this._committing=!1,this._actions=Object.create(null),this._actionSubscribers=[],this._mutations=Object.create(null),this._wrappedGetters=Object.create(null),this._modules=new La(t),this._modulesNamespaceMap=Object.create(null),this._subscribers=[],this._watcherVM=new Fa,this._makeLocalGettersCache=Object.create(null);var i=this,o=this.dispatch,s=this.commit;this.dispatch=function(t,e){return o.call(i,t,e)},this.commit=function(t,e,n){return s.call(i,t,e,n)},this.strict=r;var a=this._modules.root.state;Ha(this,a,[],this._modules.root),Ka(this,a),n.forEach((function(t){return t(e)})),(void 0!==t.devtools?t.devtools:Fa.config.devtools)&&function(t){Ea&&(t._devtoolHook=Ea,Ea.emit("vuex:init",t),Ea.on("vuex:travel-to-state",(function(e){t.replaceState(e)})),t.subscribe((function(t,e){Ea.emit("vuex:mutation",t,e)}),{prepend:!0}),t.subscribeAction((function(t,e){Ea.emit("vuex:action",t,e)}),{prepend:!0}))}(this)},Wa={state:{configurable:!0}};function qa(t,e,n){return e.indexOf(t)<0&&(n&&n.prepend?e.unshift(t):e.push(t)),function(){var n=e.indexOf(t);n>-1&&e.splice(n,1)}}function Ja(t,e){t._actions=Object.create(null),t._mutations=Object.create(null),t._wrappedGetters=Object.create(null),t._modulesNamespaceMap=Object.create(null);var n=t.state;Ha(t,n,[],t._modules.root,!0),Ka(t,n,e)}function Ka(t,e,n){var r=t._vm;t.getters={},t._makeLocalGettersCache=Object.create(null);var i=t._wrappedGetters,o={};Ia(i,(function(e,n){o[n]=function(t,e){return function(){return t(e)}}(e,t),Object.defineProperty(t.getters,n,{get:function(){return t._vm[n]},enumerable:!0})}));var s=Fa.config.silent;Fa.config.silent=!0,t._vm=new Fa({data:{$$state:e},computed:o}),Fa.config.silent=s,t.strict&&function(t){t._vm.$watch((function(){return this._data.$$state}),(function(){}),{deep:!0,sync:!0})}(t),r&&(n&&t._withCommit((function(){r._data.$$state=null})),Fa.nextTick((function(){return r.$destroy()})))}function Ha(t,e,n,r,i){var o=!n.length,s=t._modules.getNamespace(n);if(r.namespaced&&(t._modulesNamespaceMap[s],t._modulesNamespaceMap[s]=r),!o&&!i){var a=Ua(e,n.slice(0,-1)),l=n[n.length-1];t._withCommit((function(){Fa.set(a,l,r.state)}))}var c=r.context=function(t,e,n){var r=""===e,i={dispatch:r?t.dispatch:function(n,r,i){var o=Ya(n,r,i),s=o.payload,a=o.options,l=o.type;return a&&a.root||(l=e+l),t.dispatch(l,s)},commit:r?t.commit:function(n,r,i){var o=Ya(n,r,i),s=o.payload,a=o.options,l=o.type;a&&a.root||(l=e+l),t.commit(l,s,a)}};return Object.defineProperties(i,{getters:{get:r?function(){return t.getters}:function(){return function(t,e){if(!t._makeLocalGettersCache[e]){var n={},r=e.length;Object.keys(t.getters).forEach((function(i){if(i.slice(0,r)===e){var o=i.slice(r);Object.defineProperty(n,o,{get:function(){return t.getters[i]},enumerable:!0})}})),t._makeLocalGettersCache[e]=n}return t._makeLocalGettersCache[e]}(t,e)}},state:{get:function(){return Ua(t.state,n)}}}),i}(t,s,n);r.forEachMutation((function(e,n){!function(t,e,n,r){(t._mutations[e]||(t._mutations[e]=[])).push((function(e){n.call(t,r.state,e)}))}(t,s+n,e,c)})),r.forEachAction((function(e,n){var r=e.root?n:s+n,i=e.handler||e;!function(t,e,n,r){(t._actions[e]||(t._actions[e]=[])).push((function(e){var i,o=n.call(t,{dispatch:r.dispatch,commit:r.commit,getters:r.getters,state:r.state,rootGetters:t.getters,rootState:t.state},e);return(i=o)&&"function"==typeof i.then||(o=Promise.resolve(o)),t._devtoolHook?o.catch((function(e){throw t._devtoolHook.emit("vuex:error",e),e})):o}))}(t,r,i,c)})),r.forEachGetter((function(e,n){!function(t,e,n,r){if(t._wrappedGetters[e])return;t._wrappedGetters[e]=function(t){return n(r.state,r.getters,t.state,t.getters)}}(t,s+n,e,c)})),r.forEachChild((function(r,o){Ha(t,e,n.concat(o),r,i)}))}function Ua(t,e){return e.reduce((function(t,e){return t[e]}),t)}function Ya(t,e,n){return Ra(t)&&t.type&&(n=e,e=t,t=t.type),{type:t,payload:e,options:n}}function Ga(t){Fa&&t===Fa|| /*! * vuex v3.6.2 * (c) 2021 Evan You * @license MIT */ -function(t){if(Number(t.version.split(".")[0])>=2)t.mixin({beforeCreate:n});else{var e=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[n].concat(t.init):n,e.call(this,t)}}function n(){var t=this.$options;t.store?this.$store="function"==typeof t.store?t.store():t.store:t.parent&&t.parent.$store&&(this.$store=t.parent.$store)}}(xs=t)}Os.state.get=function(){return this._vm._data.$$state},Os.state.set=function(t){},_s.prototype.commit=function(t,e,n){var r=this,o=As(t,e,n),i=o.type,a=o.payload,s={type:i,payload:a},c=this._mutations[i];c&&(this._withCommit((function(){c.forEach((function(t){t(a)}))})),this._subscribers.slice().forEach((function(t){return t(s,r.state)})))},_s.prototype.dispatch=function(t,e){var n=this,r=As(t,e),o=r.type,i=r.payload,a={type:o,payload:i},s=this._actions[o];if(s){try{this._actionSubscribers.slice().filter((function(t){return t.before})).forEach((function(t){return t.before(a,n.state)}))}catch(My){}var c=s.length>1?Promise.all(s.map((function(t){return t(i)}))):s[0](i);return new Promise((function(t,e){c.then((function(e){try{n._actionSubscribers.filter((function(t){return t.after})).forEach((function(t){return t.after(a,n.state)}))}catch(My){}t(e)}),(function(t){try{n._actionSubscribers.filter((function(t){return t.error})).forEach((function(e){return e.error(a,n.state,t)}))}catch(My){}e(t)}))}))}},_s.prototype.subscribe=function(t,e){return Cs(t,this._subscribers,e)},_s.prototype.subscribeAction=function(t,e){return Cs("function"==typeof t?{before:t}:t,this._actionSubscribers,e)},_s.prototype.watch=function(t,e,n){var r=this;return this._watcherVM.$watch((function(){return t(r.state,r.getters)}),e,n)},_s.prototype.replaceState=function(t){var e=this;this._withCommit((function(){e._vm._data.$$state=t}))},_s.prototype.registerModule=function(t,e,n){void 0===n&&(n={}),"string"==typeof t&&(t=[t]),this._modules.register(t,e),Ts(this,this.state,t,this._modules.get(t),n.preserveState),Ds(this,this.state)},_s.prototype.unregisterModule=function(t){var e=this;"string"==typeof t&&(t=[t]),this._modules.unregister(t),this._withCommit((function(){var n=$s(e.state,t.slice(0,-1));xs.delete(n,t[t.length-1])})),Ms(this)},_s.prototype.hasModule=function(t){return"string"==typeof t&&(t=[t]),this._modules.isRegistered(t)},_s.prototype.hotUpdate=function(t){this._modules.update(t),Ms(this,!0)},_s.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(_s.prototype,Os);var Ns=zs((function(t,e){var n={};return Rs(e).forEach((function(e){var r=e.key,o=e.val;n[r]=function(){var e=this.$store.state,n=this.$store.getters;if(t){var r=Fs(this.$store,"mapState",t);if(!r)return;e=r.context.state,n=r.context.getters}return"function"==typeof o?o.call(this,e,n):e[o]},n[r].vuex=!0})),n})),Ps=zs((function(t,e){var n={};return Rs(e).forEach((function(e){var r=e.key,o=e.val;n[r]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var r=this.$store.commit;if(t){var i=Fs(this.$store,"mapMutations",t);if(!i)return;r=i.context.commit}return"function"==typeof o?o.apply(this,[r].concat(e)):r.apply(this.$store,[o].concat(e))}})),n})),Is=zs((function(t,e){var n={};return Rs(e).forEach((function(e){var r=e.key,o=e.val;o=t+o,n[r]=function(){if(!t||Fs(this.$store,"mapGetters",t))return this.$store.getters[o]},n[r].vuex=!0})),n})),js=zs((function(t,e){var n={};return Rs(e).forEach((function(e){var r=e.key,o=e.val;n[r]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var r=this.$store.dispatch;if(t){var i=Fs(this.$store,"mapActions",t);if(!i)return;r=i.context.dispatch}return"function"==typeof o?o.apply(this,[r].concat(e)):r.apply(this.$store,[o].concat(e))}})),n}));function Rs(t){return function(t){return Array.isArray(t)||ys(t)}(t)?Array.isArray(t)?t.map((function(t){return{key:t,val:t}})):Object.keys(t).map((function(e){return{key:e,val:t[e]}})):[]}function zs(t){return function(e,n){return"string"!=typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function Fs(t,e,n){return t._modulesNamespaceMap[n]}function Ls(t,e,n){var r=n?t.groupCollapsed:t.group;try{r.call(t,e)}catch(My){t.log(e)}}function Bs(t){try{t.groupEnd()}catch(My){t.log("—— log end ——")}}function Vs(){var t=new Date;return" @ "+Ws(t.getHours(),2)+":"+Ws(t.getMinutes(),2)+":"+Ws(t.getSeconds(),2)+"."+Ws(t.getMilliseconds(),3)}function Ws(t,e){return n="0",r=e-t.toString().length,new Array(r+1).join(n)+t;var n,r}var qs={Store:_s,install:Es,version:"3.6.2",mapState:Ns,mapMutations:Ps,mapGetters:Is,mapActions:js,createNamespacedHelpers:function(t){return{mapState:Ns.bind(null,t),mapGetters:Is.bind(null,t),mapMutations:Ps.bind(null,t),mapActions:js.bind(null,t)}},createLogger:function(t){void 0===t&&(t={});var e=t.collapsed;void 0===e&&(e=!0);var n=t.filter;void 0===n&&(n=function(t,e,n){return!0});var r=t.transformer;void 0===r&&(r=function(t){return t});var o=t.mutationTransformer;void 0===o&&(o=function(t){return t});var i=t.actionFilter;void 0===i&&(i=function(t,e){return!0});var a=t.actionTransformer;void 0===a&&(a=function(t){return t});var s=t.logMutations;void 0===s&&(s=!0);var c=t.logActions;void 0===c&&(c=!0);var l=t.logger;return void 0===l&&(l=console),function(t){var u=ms(t.state);void 0!==l&&(s&&t.subscribe((function(t,i){var a=ms(i);if(n(t,u,a)){var s=Vs(),c=o(t),f="mutation "+t.type+s;Ls(l,f,e),l.log("%c prev state","color: #9E9E9E; font-weight: bold",r(u)),l.log("%c mutation","color: #03A9F4; font-weight: bold",c),l.log("%c next state","color: #4CAF50; font-weight: bold",r(a)),Bs(l)}u=a})),c&&t.subscribeAction((function(t,n){if(i(t,n)){var r=Vs(),o=a(t),s="action "+t.type+r;Ls(l,s,e),l.log("%c action","color: #03A9F4; font-weight: bold",o),Bs(l)}})))}}};function Hs(t){return{all:t=t||new Map,on:function(e,n){var r=t.get(e);r?r.push(n):t.set(e,[n])},off:function(e,n){var r=t.get(e);r&&(n?r.splice(r.indexOf(n)>>>0,1):t.set(e,[]))},emit:function(e,n){var r=t.get(e);r&&r.slice().map((function(t){t(n)})),(r=t.get("*"))&&r.slice().map((function(t){t(e,n)}))}}}var Js,Ks,Ys="function"==typeof Map?new Map:(Js=[],Ks=[],{has:function(t){return Js.indexOf(t)>-1},get:function(t){return Ks[Js.indexOf(t)]},set:function(t,e){-1===Js.indexOf(t)&&(Js.push(t),Ks.push(e))},delete:function(t){var e=Js.indexOf(t);e>-1&&(Js.splice(e,1),Ks.splice(e,1))}}),Us=function(t){return new Event(t,{bubbles:!0})};try{new Event("test")}catch(My){Us=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!1),e}}function Xs(t){var e=Ys.get(t);e&&e.destroy()}function Gs(t){var e=Ys.get(t);e&&e.update()}var Zs=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?((Zs=function(t){return t}).destroy=function(t){return t},Zs.update=function(t){return t}):((Zs=function(t,e){return t&&Array.prototype.forEach.call(t.length?t:[t],(function(t){return function(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!Ys.has(t)){var e,n=null,r=null,o=null,i=function(){t.clientWidth!==r&&l()},a=function(e){window.removeEventListener("resize",i,!1),t.removeEventListener("input",l,!1),t.removeEventListener("keyup",l,!1),t.removeEventListener("autosize:destroy",a,!1),t.removeEventListener("autosize:update",l,!1),Object.keys(e).forEach((function(n){t.style[n]=e[n]})),Ys.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",a,!1),"onpropertychange"in t&&"oninput"in t&&t.addEventListener("keyup",l,!1),window.addEventListener("resize",i,!1),t.addEventListener("input",l,!1),t.addEventListener("autosize:update",l,!1),t.style.overflowX="hidden",t.style.wordWrap="break-word",Ys.set(t,{destroy:a,update:l}),"vertical"===(e=window.getComputedStyle(t,null)).resize?t.style.resize="none":"both"===e.resize&&(t.style.resize="horizontal"),n="content-box"===e.boxSizing?-(parseFloat(e.paddingTop)+parseFloat(e.paddingBottom)):parseFloat(e.borderTopWidth)+parseFloat(e.borderBottomWidth),isNaN(n)&&(n=0),l()}function s(e){var n=t.style.width;t.style.width="0px",t.style.width=n,t.style.overflowY=e}function c(){if(0!==t.scrollHeight){var e=function(t){for(var e=[];t&&t.parentNode&&t.parentNode instanceof Element;)t.parentNode.scrollTop&&e.push({node:t.parentNode,scrollTop:t.parentNode.scrollTop}),t=t.parentNode;return e}(t),o=document.documentElement&&document.documentElement.scrollTop;t.style.height="",t.style.height=t.scrollHeight+n+"px",r=t.clientWidth,e.forEach((function(t){t.node.scrollTop=t.scrollTop})),o&&(document.documentElement.scrollTop=o)}}function l(){c();var e=Math.round(parseFloat(t.style.height)),n=window.getComputedStyle(t,null),r="content-box"===n.boxSizing?Math.round(parseFloat(n.height)):t.offsetHeight;if(r=e?t:""+Array(e+1-r.length).join(n)+t},y={s:g,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),o=n%60;return(e<=0?"+":"-")+g(r,2,"0")+":"+g(o,2,"0")},m:function t(e,n){if(e.date()68?1900:2e3)},s=function(t){return function(e){this[t]=+e}},c=[/[+-]\d\d:?(\d\d)?|Z/,function(t){(this.zone||(this.zone={})).offset=function(t){if(!t)return 0;if("Z"===t)return 0;var e=t.match(/([+-]|\d\d)/g),n=60*e[1]+(+e[2]||0);return 0===n?0:"+"===e[0]?-n:n}(t)}],l=function(t){var e=i[t];return e&&(e.indexOf?e:e.s.concat(e.f))},u=function(t,e){var n,r=i.meridiem;if(r){for(var o=1;o<=24;o+=1)if(t.indexOf(r(o,0,e))>-1){n=o>12;break}}else n=t===(e?"pm":"PM");return n},f={A:[o,function(t){this.afternoon=u(t,!1)}],a:[o,function(t){this.afternoon=u(t,!0)}],S:[/\d/,function(t){this.milliseconds=100*+t}],SS:[n,function(t){this.milliseconds=10*+t}],SSS:[/\d{3}/,function(t){this.milliseconds=+t}],s:[r,s("seconds")],ss:[r,s("seconds")],m:[r,s("minutes")],mm:[r,s("minutes")],H:[r,s("hours")],h:[r,s("hours")],HH:[r,s("hours")],hh:[r,s("hours")],D:[r,s("day")],DD:[n,s("day")],Do:[o,function(t){var e=i.ordinal,n=t.match(/\d+/);if(this.day=n[0],e)for(var r=1;r<=31;r+=1)e(r).replace(/\[|\]/g,"")===t&&(this.day=r)}],M:[r,s("month")],MM:[n,s("month")],MMM:[o,function(t){var e=l("months"),n=(l("monthsShort")||e.map((function(t){return t.substr(0,3)}))).indexOf(t)+1;if(n<1)throw new Error;this.month=n%12||n}],MMMM:[o,function(t){var e=l("months").indexOf(t)+1;if(e<1)throw new Error;this.month=e%12||e}],Y:[/[+-]?\d+/,s("year")],YY:[n,function(t){this.year=a(t)}],YYYY:[/\d{4}/,s("year")],Z:c,ZZ:c};function p(n){var r,o;r=n,o=i&&i.formats;for(var a=(n=r.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,(function(e,n,r){var i=r&&r.toUpperCase();return n||o[r]||t[r]||o[i].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,(function(t,e,n){return e||n.slice(1)}))}))).match(e),s=a.length,c=0;c-1)return new Date(("X"===e?1e3:1)*t);var r=p(e)(t),o=r.year,i=r.month,a=r.day,s=r.hours,c=r.minutes,l=r.seconds,u=r.milliseconds,f=r.zone,d=new Date,h=a||(o||i?1:d.getDate()),v=o||d.getFullYear(),m=0;o&&!i||(m=i>0?i-1:d.getMonth());var g=s||0,y=c||0,b=l||0,w=u||0;return f?new Date(Date.UTC(v,m,h,g,y,b,w+60*f.offset*1e3)):n?new Date(Date.UTC(v,m,h,g,y,b,w)):new Date(v,m,h,g,y,b,w)}catch(x){return new Date("")}}(e,s,r),this.init(),f&&!0!==f&&(this.$L=this.locale(f).$L),u&&e!=this.format(s)&&(this.$d=new Date("")),i={}}else if(s instanceof Array)for(var d=s.length,h=1;h<=d;h+=1){a[1]=s[h-1];var v=n.apply(this,a);if(v.isValid()){this.$d=v.$d,this.$L=v.$L,this.init();break}h===d&&(this.$d=new Date(""))}else o.call(this,t)}}}();function ic(t){return(ic="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var ac={selector:"vue-portal-target-".concat(((t=21)=>{let e="",n=t;for(;n--;)e+="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"[64*Math.random()|0];return e})())},sc=function(t){return ac.selector=t},cc="undefined"!=typeof window&&void 0!==("undefined"==typeof document?"undefined":ic(document)),lc=Cn.extend({abstract:!0,name:"PortalOutlet",props:["nodes","tag"],data:function(t){return{updatedNodes:t.nodes}},render:function(t){var e=this.updatedNodes&&this.updatedNodes();return e?1!==e.length||e[0].text?t(this.tag||"DIV",e):e:t()},destroyed:function(){var t=this.$el;t&&t.parentNode.removeChild(t)}}),uc=Cn.extend({name:"VueSimplePortal",props:{disabled:{type:Boolean},prepend:{type:Boolean},selector:{type:String,default:function(){return"#".concat(ac.selector)}},tag:{type:String,default:"DIV"}},render:function(t){if(this.disabled){var e=this.$scopedSlots&&this.$scopedSlots.default();return e?e.length<2&&!e[0].text?e:t(this.tag,e):t()}return t()},created:function(){this.getTargetEl()||this.insertTargetEl()},updated:function(){var t=this;this.$nextTick((function(){t.disabled||t.slotFn===t.$scopedSlots.default||(t.container.updatedNodes=t.$scopedSlots.default),t.slotFn=t.$scopedSlots.default}))},beforeDestroy:function(){this.unmount()},watch:{disabled:{immediate:!0,handler:function(t){t?this.unmount():this.$nextTick(this.mount)}}},methods:{getTargetEl:function(){if(cc)return document.querySelector(this.selector)},insertTargetEl:function(){if(cc){var t=document.querySelector("body"),e=document.createElement(this.tag);e.id=this.selector.substring(1),t.appendChild(e)}},mount:function(){if(cc){var t=this.getTargetEl(),e=document.createElement("DIV");this.prepend&&t.firstChild?t.insertBefore(e,t.firstChild):t.appendChild(e),this.container=new lc({el:e,parent:this,propsData:{tag:this.tag,nodes:this.$scopedSlots.default}})}},unmount:function(){this.container&&(this.container.$destroy(),delete this.container)}}});function fc(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t.component(e.name||"portal",uc),e.defaultSelector&&sc(e.defaultSelector)}"undefined"!=typeof window&&window.Vue&&window.Vue===Cn&&Cn.use(fc);var pc={},dc={};function hc(t){return null==t}function vc(t){return null!=t}function mc(t,e){return e.tag===t.tag&&e.key===t.key}function gc(t){var e=t.tag;t.vm=new e({data:t.args})}function yc(t,e,n){var r,o,i={};for(r=e;r<=n;++r)vc(o=t[r].key)&&(i[o]=r);return i}function bc(t,e,n){for(;e<=n;++e)gc(t[e])}function wc(t,e,n){for(;e<=n;++e){var r=t[e];vc(r)&&(r.vm.$destroy(),r.vm=null)}}function xc(t,e){t!==e&&(e.vm=t.vm,function(t){for(var e=Object.keys(t.args),n=0;ns?bc(e,a,u):a>u&&wc(t,i,s)}(t,e):vc(e)?bc(e,0,e.length-1):vc(t)&&wc(t,0,t.length-1)};var Sc={};function kc(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function _c(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n1?s:s.$sub[0]:null}}},computed:{run:function(){var t=this,e=this.lazyParentModel();if(Array.isArray(e)&&e.__ob__){var n=e.__ob__.dep;n.depend();var r=n.constructor.target;if(!this._indirectWatcher){var o=r.constructor;this._indirectWatcher=new o(this,(function(){return t.runRule(e)}),null,{lazy:!0})}var i=this.getModel();if(!this._indirectWatcher.dirty&&this._lastModel===i)return this._indirectWatcher.depend(),r.value;this._lastModel=i,this._indirectWatcher.evaluate(),this._indirectWatcher.depend()}else this._indirectWatcher&&(this._indirectWatcher.teardown(),this._indirectWatcher=null);return this._indirectWatcher?this._indirectWatcher.value:this.runRule(e)},$params:function(){return this.run.params},proxy:function(){var t=this.run.output;return t.__isVuelidateAsyncVm?!!t.v:!!t},$pending:function(){var t=this.run.output;return!!t.__isVuelidateAsyncVm&&t.p}},destroyed:function(){this._indirectWatcher&&(this._indirectWatcher.teardown(),this._indirectWatcher=null)}}),s=o.extend({data:function(){return{dirty:!1,validations:null,lazyModel:null,model:null,prop:null,lazyParentModel:null,rootModel:null}},methods:a(a({},m),{},{refProxy:function(t){return this.getRef(t).proxy},getRef:function(t){return this.refs[t]},isNested:function(t){return"function"!=typeof this.validations[t]}}),computed:a(a({},h),{},{nestedKeys:function(){return this.keys.filter(this.isNested)},ruleKeys:function(){var t=this;return this.keys.filter((function(e){return!t.isNested(e)}))},keys:function(){return Object.keys(this.validations).filter((function(t){return"$params"!==t}))},proxy:function(){var t=this,e=u(this.keys,(function(e){return{enumerable:!0,configurable:!0,get:function(){return t.refProxy(e)}}})),n=u(g,(function(e){return{enumerable:!0,configurable:!0,get:function(){return t[e]}}})),r=u(y,(function(e){return{enumerable:!1,configurable:!0,get:function(){return t[e]}}})),o=this.hasIter()?{$iter:{enumerable:!0,value:Object.defineProperties({},a({},e))}}:{};return Object.defineProperties({},a(a(a(a({},e),o),{},{$model:{enumerable:!0,get:function(){var e=t.lazyParentModel();return null!=e?e[t.prop]:null},set:function(e){var n=t.lazyParentModel();null!=n&&(n[t.prop]=e,t.$touch())}}},n),r))},children:function(){var t=this;return[].concat(r(this.nestedKeys.map((function(e){return w(t,e)}))),r(this.ruleKeys.map((function(e){return x(t,e)})))).filter(Boolean)}})}),c=s.extend({methods:{isNested:function(t){return void 0!==this.validations[t]()},getRef:function(t){var e=this;return{get proxy(){return e.validations[t]()||!1}}}}}),v=s.extend({computed:{keys:function(){var t=this.getModel();return p(t)?Object.keys(t):[]},tracker:function(){var t=this,e=this.validations.$trackBy;return e?function(n){return"".concat(d(t.rootModel,t.getModelKey(n),e))}:function(t){return"".concat(t)}},getModelLazy:function(){var t=this;return function(){return t.getModel()}},children:function(){var t=this,n=this.validations,r=this.getModel(),o=a({},n);delete o.$trackBy;var i={};return this.keys.map((function(n){var a=t.tracker(n);return i.hasOwnProperty(a)?null:(i[a]=!0,(0,e.h)(s,a,{validations:o,prop:n,lazyParentModel:t.getModelLazy,model:r[n],rootModel:t.rootModel}))})).filter(Boolean)}},methods:{isNested:function(){return!0},getRef:function(t){return this.refs[this.tracker(t)]},hasIter:function(){return!0}}}),w=function(t,n){if("$each"===n)return(0,e.h)(v,n,{validations:t.validations[n],lazyParentModel:t.lazyParentModel,prop:n,lazyModel:t.getModel,rootModel:t.rootModel});var r=t.validations[n];if(Array.isArray(r)){var o=t.rootModel,i=u(r,(function(t){return function(){return d(o,o.$v,t)}}),(function(t){return Array.isArray(t)?t.join("."):t}));return(0,e.h)(c,n,{validations:i,lazyParentModel:l,prop:n,lazyModel:l,rootModel:o})}return(0,e.h)(s,n,{validations:r,lazyParentModel:t.getModel,prop:n,lazyModel:t.getModelKey,rootModel:t.rootModel})},x=function(t,n){return(0,e.h)(i,n,{rule:t.validations[n],lazyParentModel:t.lazyParentModel,lazyModel:t.getModel,rootModel:t.rootModel})};return b={VBase:o,Validation:s}},x=null;var S=function(t,n){var r=function(t){if(x)return x;for(var e=t.constructor;e.super;)e=e.super;return x=e,e}(t),o=w(r),i=o.Validation;return new(0,o.VBase)({computed:{children:function(){var r="function"==typeof n?n.call(t):n;return[(0,e.h)(i,"$v",{validations:r,lazyParentModel:l,prop:"$v",model:t,rootModel:t})]}}})},k={data:function(){var t=this.$options.validations;return t&&(this._vuelidate=S(this,t)),{}},beforeCreate:function(){var t=this.$options;t.validations&&(t.computed||(t.computed={}),t.computed.$v||(t.computed.$v=function(){return this._vuelidate?this._vuelidate.refs.$v.proxy:null}))},beforeDestroy:function(){this._vuelidate&&(this._vuelidate.$destroy(),this._vuelidate=null)}};function _(t){t.mixin(k)}t.validationMixin=k;var O=_;t.default=O}(pc);var Nc=tc(pc);function Pc(t){this.content=t}Pc.prototype={constructor:Pc,find:function(t){for(var e=0;e>1}},Pc.from=function(t){if(t instanceof Pc)return t;var e=[];if(t)for(var n in t)e.push(n,t[n]);return new Pc(e)};var Ic=Pc;function jc(t,e,n){for(var r=0;;r++){if(r==t.childCount||r==e.childCount)return t.childCount==e.childCount?null:n;var o=t.child(r),i=e.child(r);if(o!=i){if(!o.sameMarkup(i))return n;if(o.isText&&o.text!=i.text){for(var a=0;o.text[a]==i.text[a];a++)n++;return n}if(o.content.size||i.content.size){var s=jc(o.content,i.content,n+1);if(null!=s)return s}n+=o.nodeSize}else n+=o.nodeSize}}function Rc(t,e,n,r){for(var o=t.childCount,i=e.childCount;;){if(0==o||0==i)return o==i?null:{a:n,b:r};var a=t.child(--o),s=e.child(--i),c=a.nodeSize;if(a!=s){if(!a.sameMarkup(s))return{a:n,b:r};if(a.isText&&a.text!=s.text){for(var l=0,u=Math.min(a.text.length,s.text.length);lt&&!1!==n(s,r+a,o,i)&&s.content.size){var l=a+1;s.nodesBetween(Math.max(0,t-l),Math.min(s.content.size,e-l),n,r+l)}a=c}},zc.prototype.descendants=function(t){this.nodesBetween(0,this.size,t)},zc.prototype.textBetween=function(t,e,n,r){var o="",i=!0;return this.nodesBetween(t,e,(function(a,s){a.isText?(o+=a.text.slice(Math.max(t,s)-s,e-s),i=!n):a.isLeaf&&r?(o+="function"==typeof r?r(a):r,i=!n):!i&&a.isBlock&&(o+=n,i=!0)}),0),o},zc.prototype.append=function(t){if(!t.size)return this;if(!this.size)return t;var e=this.lastChild,n=t.firstChild,r=this.content.slice(),o=0;for(e.isText&&e.sameMarkup(n)&&(r[r.length-1]=e.withText(e.text+n.text),o=1);ot)for(var o=0,i=0;it&&((ie)&&(a=a.isText?a.cut(Math.max(0,t-i),Math.min(a.text.length,e-i)):a.cut(Math.max(0,t-i-1),Math.min(a.content.size,e-i-1))),n.push(a),r+=a.nodeSize),i=s}return new zc(n,r)},zc.prototype.cutByIndex=function(t,e){return t==e?zc.empty:0==t&&e==this.content.length?this:new zc(this.content.slice(t,e))},zc.prototype.replaceChild=function(t,e){var n=this.content[t];if(n==e)return this;var r=this.content.slice(),o=this.size+e.nodeSize-n.nodeSize;return r[t]=e,new zc(r,o)},zc.prototype.addToStart=function(t){return new zc([t].concat(this.content),this.size+t.nodeSize)},zc.prototype.addToEnd=function(t){return new zc(this.content.concat(t),this.size+t.nodeSize)},zc.prototype.eq=function(t){if(this.content.length!=t.content.length)return!1;for(var e=0;ethis.size||t<0)throw new RangeError("Position "+t+" outside of fragment ("+this+")");for(var n=0,r=0;;n++){var o=r+this.child(n).nodeSize;if(o>=t)return o==t||e>0?Bc(n+1,o):Bc(n,r);r=o}},zc.prototype.toString=function(){return"<"+this.toStringInner()+">"},zc.prototype.toStringInner=function(){return this.content.join(", ")},zc.prototype.toJSON=function(){return this.content.length?this.content.map((function(t){return t.toJSON()})):null},zc.fromJSON=function(t,e){if(!e)return zc.empty;if(!Array.isArray(e))throw new RangeError("Invalid input for Fragment.fromJSON");return new zc(e.map(t.nodeFromJSON))},zc.fromArray=function(t){if(!t.length)return zc.empty;for(var e,n=0,r=0;rthis.type.rank&&(e||(e=t.slice(0,r)),e.push(this),n=!0),e&&e.push(o)}}return e||(e=t.slice()),n||e.push(this),e},Wc.prototype.removeFromSet=function(t){for(var e=0;et.depth)throw new qc("Inserted content deeper than insertion position");if(t.depth-n.openStart!=e.depth-n.openEnd)throw new qc("Inconsistent open depths");return Xc(t,e,n,0)}function Xc(t,e,n,r){var o=t.index(r),i=t.node(r);if(o==e.index(r)&&r=0;o--)r=e.node(o).copy(zc.from(r));return{start:r.resolveNoCache(t.openStart+n),end:r.resolveNoCache(r.content.size-t.openEnd-n)}}(n,t);return el(i,nl(t,s.start,s.end,e,r))}var c=t.parent,l=c.content;return el(c,l.cut(0,t.parentOffset).append(n.content).append(l.cut(e.parentOffset)))}return el(i,rl(t,e,r))}function Gc(t,e){if(!e.type.compatibleContent(t.type))throw new qc("Cannot join "+e.type.name+" onto "+t.type.name)}function Zc(t,e,n){var r=t.node(n);return Gc(r,e.node(n)),r}function Qc(t,e){var n=e.length-1;n>=0&&t.isText&&t.sameMarkup(e[n])?e[n]=t.withText(e[n].text+t.text):e.push(t)}function tl(t,e,n,r){var o=(e||t).node(n),i=0,a=e?e.index(n):o.childCount;t&&(i=t.index(n),t.depth>n?i++:t.textOffset&&(Qc(t.nodeAfter,r),i++));for(var s=i;so&&Zc(t,e,o+1),a=r.depth>o&&Zc(n,r,o+1),s=[];return tl(null,t,o,s),i&&a&&e.index(o)==n.index(o)?(Gc(i,a),Qc(el(i,nl(t,e,n,r,o+1)),s)):(i&&Qc(el(i,rl(t,e,o+1)),s),tl(e,n,o,s),a&&Qc(el(a,rl(n,r,o+1)),s)),tl(r,null,o,s),new zc(s)}function rl(t,e,n){var r=[];(tl(null,t,n,r),t.depth>n)&&Qc(el(Zc(t,e,n+1),rl(t,e,n+1)),r);return tl(e,null,n,r),new zc(r)}Jc.size.get=function(){return this.content.size-this.openStart-this.openEnd},Hc.prototype.insertAt=function(t,e){var n=Yc(this.content,t+this.openStart,e,null);return n&&new Hc(n,this.openStart,this.openEnd)},Hc.prototype.removeBetween=function(t,e){return new Hc(Kc(this.content,t+this.openStart,e+this.openStart),this.openStart,this.openEnd)},Hc.prototype.eq=function(t){return this.content.eq(t.content)&&this.openStart==t.openStart&&this.openEnd==t.openEnd},Hc.prototype.toString=function(){return this.content+"("+this.openStart+","+this.openEnd+")"},Hc.prototype.toJSON=function(){if(!this.content.size)return null;var t={content:this.content.toJSON()};return this.openStart>0&&(t.openStart=this.openStart),this.openEnd>0&&(t.openEnd=this.openEnd),t},Hc.fromJSON=function(t,e){if(!e)return Hc.empty;var n=e.openStart||0,r=e.openEnd||0;if("number"!=typeof n||"number"!=typeof r)throw new RangeError("Invalid input for Slice.fromJSON");return new Hc(zc.fromJSON(t,e.content),n,r)},Hc.maxOpen=function(t,e){void 0===e&&(e=!0);for(var n=0,r=0,o=t.firstChild;o&&!o.isLeaf&&(e||!o.type.spec.isolating);o=o.firstChild)n++;for(var i=t.lastChild;i&&!i.isLeaf&&(e||!i.type.spec.isolating);i=i.lastChild)r++;return new Hc(t,n,r)},Object.defineProperties(Hc.prototype,Jc),Hc.empty=new Hc(zc.empty,0,0);var ol=function(t,e,n){this.pos=t,this.path=e,this.depth=e.length/3-1,this.parentOffset=n},il={parent:{configurable:!0},doc:{configurable:!0},textOffset:{configurable:!0},nodeAfter:{configurable:!0},nodeBefore:{configurable:!0}};ol.prototype.resolveDepth=function(t){return null==t?this.depth:t<0?this.depth+t:t},il.parent.get=function(){return this.node(this.depth)},il.doc.get=function(){return this.node(0)},ol.prototype.node=function(t){return this.path[3*this.resolveDepth(t)]},ol.prototype.index=function(t){return this.path[3*this.resolveDepth(t)+1]},ol.prototype.indexAfter=function(t){return t=this.resolveDepth(t),this.index(t)+(t!=this.depth||this.textOffset?1:0)},ol.prototype.start=function(t){return 0==(t=this.resolveDepth(t))?0:this.path[3*t-1]+1},ol.prototype.end=function(t){return t=this.resolveDepth(t),this.start(t)+this.node(t).content.size},ol.prototype.before=function(t){if(!(t=this.resolveDepth(t)))throw new RangeError("There is no position before the top-level node");return t==this.depth+1?this.pos:this.path[3*t-1]},ol.prototype.after=function(t){if(!(t=this.resolveDepth(t)))throw new RangeError("There is no position after the top-level node");return t==this.depth+1?this.pos:this.path[3*t-1]+this.path[3*t].nodeSize},il.textOffset.get=function(){return this.pos-this.path[this.path.length-1]},il.nodeAfter.get=function(){var t=this.parent,e=this.index(this.depth);if(e==t.childCount)return null;var n=this.pos-this.path[this.path.length-1],r=t.child(e);return n?t.child(e).cut(n):r},il.nodeBefore.get=function(){var t=this.index(this.depth),e=this.pos-this.path[this.path.length-1];return e?this.parent.child(t).cut(0,e):0==t?null:this.parent.child(t-1)},ol.prototype.posAtIndex=function(t,e){e=this.resolveDepth(e);for(var n=this.path[3*e],r=0==e?0:this.path[3*e-1]+1,o=0;o0;e--)if(this.start(e)<=t&&this.end(e)>=t)return e;return 0},ol.prototype.blockRange=function(t,e){if(void 0===t&&(t=this),t.pos=0;n--)if(t.pos<=this.end(n)&&(!e||e(this.node(n))))return new ll(this,t,n)},ol.prototype.sameParent=function(t){return this.pos-this.parentOffset==t.pos-t.parentOffset},ol.prototype.max=function(t){return t.pos>this.pos?t:this},ol.prototype.min=function(t){return t.pos=0&&e<=t.content.size))throw new RangeError("Position "+e+" out of range");for(var n=[],r=0,o=e,i=t;;){var a=i.content.findIndex(o),s=a.index,c=a.offset,l=o-c;if(n.push(i,s,r+c),!l)break;if((i=i.child(s)).isText)break;o=l-1,r+=c+1}return new ol(e,n,o)},ol.resolveCached=function(t,e){for(var n=0;nt&&this.nodesBetween(t,e,(function(t){return n.isInSet(t.marks)&&(r=!0),!r})),r},dl.isBlock.get=function(){return this.type.isBlock},dl.isTextblock.get=function(){return this.type.isTextblock},dl.inlineContent.get=function(){return this.type.inlineContent},dl.isInline.get=function(){return this.type.isInline},dl.isText.get=function(){return this.type.isText},dl.isLeaf.get=function(){return this.type.isLeaf},dl.isAtom.get=function(){return this.type.isAtom},pl.prototype.toString=function(){if(this.type.spec.toDebugString)return this.type.spec.toDebugString(this);var t=this.type.name;return this.content.size&&(t+="("+this.content.toStringInner()+")"),vl(this.marks,t)},pl.prototype.contentMatchAt=function(t){var e=this.type.contentMatch.matchFragment(this.content,0,t);if(!e)throw new Error("Called contentMatchAt on a node with invalid content");return e},pl.prototype.canReplace=function(t,e,n,r,o){void 0===n&&(n=zc.empty),void 0===r&&(r=0),void 0===o&&(o=n.childCount);var i=this.contentMatchAt(t).matchFragment(n,r,o),a=i&&i.matchFragment(this.content,e);if(!a||!a.validEnd)return!1;for(var s=r;s=0;n--)e=t[n].type.name+"("+e+")";return e}var ml=function(t){this.validEnd=t,this.next=[],this.wrapCache=[]},gl={inlineContent:{configurable:!0},defaultType:{configurable:!0},edgeCount:{configurable:!0}};ml.parse=function(t,e){var n=new yl(t,e);if(null==n.next)return ml.empty;var r=wl(n);n.next&&n.err("Unexpected trailing text");var o=function(t){var e=Object.create(null);return n(Cl(t,0));function n(r){var o=[];r.forEach((function(e){t[e].forEach((function(e){var n=e.term,r=e.to;if(n){var i=o.indexOf(n),a=i>-1&&o[i+1];Cl(t,r).forEach((function(t){a||o.push(n,a=[]),-1==a.indexOf(t)&&a.push(t)}))}}))}));for(var i=e[r.join(",")]=new ml(r.indexOf(t.length-1)>-1),a=0;a>1},ml.prototype.edge=function(t){var e=t<<1;if(e>=this.next.length)throw new RangeError("There's no "+t+"th edge in this content match");return{type:this.next[e],next:this.next[e+1]}},ml.prototype.toString=function(){var t=[];return function e(n){t.push(n);for(var r=1;r"+t.indexOf(e.next[o+1]);return r})).join("\n")},Object.defineProperties(ml.prototype,gl),ml.empty=new ml(!0);var yl=function(t,e){this.string=t,this.nodeTypes=e,this.inline=null,this.pos=0,this.tokens=t.split(/\s*(?=\b|\W|$)/),""==this.tokens[this.tokens.length-1]&&this.tokens.pop(),""==this.tokens[0]&&this.tokens.shift()},bl={next:{configurable:!0}};function wl(t){var e=[];do{e.push(xl(t))}while(t.eat("|"));return 1==e.length?e[0]:{type:"choice",exprs:e}}function xl(t){var e=[];do{e.push(Sl(t))}while(t.next&&")"!=t.next&&"|"!=t.next);return 1==e.length?e[0]:{type:"seq",exprs:e}}function Sl(t){for(var e=function(t){if(t.eat("(")){var e=wl(t);return t.eat(")")||t.err("Missing closing paren"),e}if(!/\W/.test(t.next)){var n=function(t,e){var n=t.nodeTypes,r=n[e];if(r)return[r];var o=[];for(var i in n){var a=n[i];a.groups.indexOf(e)>-1&&o.push(a)}0==o.length&&t.err("No node type or group '"+e+"' found");return o}(t,t.next).map((function(e){return null==t.inline?t.inline=e.isInline:t.inline!=e.isInline&&t.err("Mixing inline and block content"),{type:"name",value:e}}));return t.pos++,1==n.length?n[0]:{type:"choice",exprs:n}}t.err("Unexpected token '"+t.next+"'")}(t);;)if(t.eat("+"))e={type:"plus",expr:e};else if(t.eat("*"))e={type:"star",expr:e};else if(t.eat("?"))e={type:"opt",expr:e};else{if(!t.eat("{"))break;e=_l(t,e)}return e}function kl(t){/\D/.test(t.next)&&t.err("Expected number, got '"+t.next+"'");var e=Number(t.next);return t.pos++,e}function _l(t,e){var n=kl(t),r=n;return t.eat(",")&&(r="}"!=t.next?kl(t):-1),t.eat("}")||t.err("Unclosed braced range"),{type:"range",min:n,max:r,expr:e}}function Ol(t,e){return e-t}function Cl(t,e){var n=[];return function e(r){var o=t[r];if(1==o.length&&!o[0].term)return e(o[0].to);n.push(r);for(var i=0;i-1},$l.prototype.allowsMarks=function(t){if(null==this.markSet)return!0;for(var e=0;e-1};var Il=function(t){for(var e in this.spec={},t)this.spec[e]=t[e];this.spec.nodes=Ic.from(t.nodes),this.spec.marks=Ic.from(t.marks),this.nodes=$l.compile(this.spec.nodes,this),this.marks=Pl.compile(this.spec.marks,this);var n=Object.create(null);for(var r in this.nodes){if(r in this.marks)throw new RangeError(r+" can not be both a node and a mark");var o=this.nodes[r],i=o.spec.content||"",a=o.spec.marks;o.contentMatch=n[i]||(n[i]=ml.parse(i,this.nodes)),o.inlineContent=o.contentMatch.inlineContent,o.markSet="_"==a?null:a?jl(this,a.split(" ")):""!=a&&o.inlineContent?null:[]}for(var s in this.marks){var c=this.marks[s],l=c.spec.excludes;c.excluded=null==l?[c]:""==l?[]:jl(this,l.split(" "))}this.nodeFromJSON=this.nodeFromJSON.bind(this),this.markFromJSON=this.markFromJSON.bind(this),this.topNodeType=this.nodes[this.spec.topNode||"doc"],this.cached=Object.create(null),this.cached.wrappings=Object.create(null)};function jl(t,e){for(var n=[],r=0;r-1)&&n.push(a=c)}if(!a)throw new SyntaxError("Unknown mark type: '"+e[r]+"'")}return n}Il.prototype.node=function(t,e,n,r){if("string"==typeof t)t=this.nodeType(t);else{if(!(t instanceof $l))throw new RangeError("Invalid node type: "+t);if(t.schema!=this)throw new RangeError("Node type from different schema used ("+t.name+")")}return t.createChecked(e,n,r)},Il.prototype.text=function(t,e){var n=this.nodes.text;return new hl(n,n.defaultAttrs,t,Wc.setFrom(e))},Il.prototype.mark=function(t,e){return"string"==typeof t&&(t=this.marks[t]),t.create(e)},Il.prototype.nodeFromJSON=function(t){return pl.fromJSON(this,t)},Il.prototype.markFromJSON=function(t){return Wc.fromJSON(this,t)},Il.prototype.nodeType=function(t){var e=this.nodes[t];if(!e)throw new RangeError("Unknown node type: "+t);return e};var Rl=function(t,e){var n=this;this.schema=t,this.rules=e,this.tags=[],this.styles=[],e.forEach((function(t){t.tag?n.tags.push(t):t.style&&n.styles.push(t)})),this.normalizeLists=!this.tags.some((function(e){if(!/^(ul|ol)\b/.test(e.tag)||!e.node)return!1;var n=t.nodes[e.node];return n.contentMatch.matchType(n)}))};Rl.prototype.parse=function(t,e){void 0===e&&(e={});var n=new Wl(this,e,!1);return n.addAll(t,null,e.from,e.to),n.finish()},Rl.prototype.parseSlice=function(t,e){void 0===e&&(e={});var n=new Wl(this,e,!0);return n.addAll(t,null,e.from,e.to),Hc.maxOpen(n.finish())},Rl.prototype.matchTag=function(t,e,n){for(var r=n?this.tags.indexOf(n)+1:0;rt.length&&(61!=i.style.charCodeAt(t.length)||i.style.slice(t.length+1)!=e))){if(i.getAttrs){var a=i.getAttrs(e);if(!1===a)continue;i.attrs=a}return i}}},Rl.schemaRules=function(t){var e=[];function n(t){for(var n=null==t.priority?50:t.priority,r=0;r=0;e--)if(t.eq(this.stashMarks[e]))return this.stashMarks.splice(e,1)[0]},Vl.prototype.applyPending=function(t){for(var e=0,n=this.pendingMarks;e=0;r--){var o=this.nodes[r],i=o.findWrapping(t);if(i&&(!e||e.length>i.length)&&(e=i,n=o,!i.length))break;if(o.solid)break}if(!e)return!1;this.sync(n);for(var a=0;athis.open){for(;e>this.open;e--)this.nodes[e-1].content.push(this.nodes[e].finish(t));this.nodes.length=this.open+1}},Wl.prototype.finish=function(){return this.open=0,this.closeExtra(this.isOpen),this.nodes[0].finish(this.isOpen||this.options.topOpen)},Wl.prototype.sync=function(t){for(var e=this.open;e>=0;e--)if(this.nodes[e]==t)return void(this.open=e)},ql.currentPos.get=function(){this.closeExtra();for(var t=0,e=this.open;e>=0;e--){for(var n=this.nodes[e].content,r=n.length-1;r>=0;r--)t+=n[r].nodeSize;e&&t++}return t},Wl.prototype.findAtPoint=function(t,e){if(this.find)for(var n=0;n-1)return t.split(/\s*\|\s*/).some(this.matchesContext,this);var n=t.split("/"),r=this.options.context,o=!(this.isOpen||r&&r.parent.type!=this.nodes[0].type),i=-(r?r.depth+1:0)+(o?0:1),a=function(t,s){for(;t>=0;t--){var c=n[t];if(""==c){if(t==n.length-1||0==t)continue;for(;s>=i;s--)if(a(t-1,s))return!0;return!1}var l=s>0||0==s&&o?e.nodes[s].type:r&&s>=i?r.node(s-i).type:null;if(!l||l.name!=c&&-1==l.groups.indexOf(c))return!1;s--}return!0};return a(n.length-1,this.open)},Wl.prototype.textblockFromContext=function(){var t=this.options.context;if(t)for(var e=t.depth;e>=0;e--){var n=t.node(e).contentMatchAt(t.indexAfter(e)).defaultType;if(n&&n.isTextblock&&n.defaultAttrs)return n}for(var r in this.parser.schema.nodes){var o=this.parser.schema.nodes[r];if(o.isTextblock&&o.defaultAttrs)return o}},Wl.prototype.addPendingMark=function(t){var e=function(t,e){for(var n=0;n=0;n--){var r=this.nodes[n];if(r.pendingMarks.lastIndexOf(t)>-1)r.pendingMarks=t.removeFromSet(r.pendingMarks);else{r.activeMarks=t.removeFromSet(r.activeMarks);var o=r.popFromStashMark(t);o&&r.type&&r.type.allowsMarkType(o.type)&&(r.activeMarks=o.addToSet(r.activeMarks))}if(r==e)break}},Object.defineProperties(Wl.prototype,ql);var Yl=function(t,e){this.nodes=t||{},this.marks=e||{}};function Ul(t){var e={};for(var n in t){var r=t[n].spec.toDOM;r&&(e[n]=r)}return e}function Xl(t){return t.document||window.document}Yl.prototype.serializeFragment=function(t,e,n){var r=this;void 0===e&&(e={}),n||(n=Xl(e).createDocumentFragment());var o=n,i=null;return t.forEach((function(t){if(i||t.marks.length){i||(i=[]);for(var n=0,a=0;n=0;r--){var o=this.serializeMark(t.marks[r],t.isInline,e);o&&((o.contentDOM||o.dom).appendChild(n),n=o.dom)}return n},Yl.prototype.serializeMark=function(t,e,n){void 0===n&&(n={});var r=this.marks[t.type.name];return r&&Yl.renderSpec(Xl(n),r(t,e))},Yl.renderSpec=function(t,e,n){if(void 0===n&&(n=null),"string"==typeof e)return{dom:t.createTextNode(e)};if(null!=e.nodeType)return{dom:e};if(e.dom&&null!=e.dom.nodeType)return e;var r=e[0],o=r.indexOf(" ");o>0&&(n=r.slice(0,o),r=r.slice(o+1));var i=null,a=n?t.createElementNS(n,r):t.createElement(r),s=e[1],c=1;if(s&&"object"==typeof s&&null==s.nodeType&&!Array.isArray(s))for(var l in c=2,s)if(null!=s[l]){var u=l.indexOf(" ");u>0?a.setAttributeNS(l.slice(0,u),l.slice(u+1),s[l]):a.setAttribute(l,s[l])}for(var f=c;fc)throw new RangeError("Content hole must be the only child of its parent node");return{dom:a,contentDOM:a}}var d=Yl.renderSpec(t,p,n),h=d.dom,v=d.contentDOM;if(a.appendChild(h),v){if(i)throw new RangeError("Multiple content holes");i=v}}return{dom:a,contentDOM:i}},Yl.fromSchema=function(t){return t.cached.domSerializer||(t.cached.domSerializer=new Yl(this.nodesFromSchema(t),this.marksFromSchema(t)))},Yl.nodesFromSchema=function(t){var e=Ul(t.nodes);return e.text||(e.text=function(t){return t.text}),e},Yl.marksFromSchema=function(t){return Ul(t.marks)};var Gl=Math.pow(2,16);function Zl(t){return 65535&t}var Ql=function(t,e,n){void 0===e&&(e=!1),void 0===n&&(n=null),this.pos=t,this.deleted=e,this.recover=n},tu=function(t,e){void 0===e&&(e=!1),this.ranges=t,this.inverted=e};tu.prototype.recover=function(t){var e=0,n=Zl(t);if(!this.inverted)for(var r=0;rt)break;var c=this.ranges[a+o],l=this.ranges[a+i],u=s+c;if(t<=u){var f=s+r+((c?t==s?-1:t==u?1:e:e)<0?0:l);if(n)return f;var p=t==(e<0?s:u)?null:a/3+(t-s)*Gl;return new Ql(f,e<0?t!=s:t!=u,p)}r+=l-c}return n?t+r:new Ql(t+r)},tu.prototype.touches=function(t,e){for(var n=0,r=Zl(e),o=this.inverted?2:1,i=this.inverted?1:2,a=0;at)break;var c=this.ranges[a+o];if(t<=s+c&&a==3*r)return!0;n+=this.ranges[a+i]-c}return!1},tu.prototype.forEach=function(t){for(var e=this.inverted?2:1,n=this.inverted?1:2,r=0,o=0;r=0;e--){var r=t.getMirror(e);this.appendMap(t.maps[e].invert(),null!=r&&r>e?n-r-1:null)}},eu.prototype.invert=function(){var t=new eu;return t.appendMappingInverted(this),t},eu.prototype.map=function(t,e){if(void 0===e&&(e=1),this.mirror)return this._map(t,e,!0);for(var n=this.from;no&&a0},ru.prototype.addStep=function(t,e){this.docs.push(this.doc),this.steps.push(t),this.mapping.appendMap(t.getMap()),this.doc=e},Object.defineProperties(ru.prototype,ou);var au=Object.create(null),su=function(){};su.prototype.apply=function(t){return iu()},su.prototype.getMap=function(){return tu.empty},su.prototype.invert=function(t){return iu()},su.prototype.map=function(t){return iu()},su.prototype.merge=function(t){return null},su.prototype.toJSON=function(){return iu()},su.fromJSON=function(t,e){if(!e||!e.stepType)throw new RangeError("Invalid input for Step.fromJSON");var n=au[e.stepType];if(!n)throw new RangeError("No step type "+e.stepType+" defined");return n.fromJSON(t,e)},su.jsonID=function(t,e){if(t in au)throw new RangeError("Duplicate use of step JSON ID "+t);return au[t]=e,e.prototype.jsonID=t,e};var cu=function(t,e){this.doc=t,this.failed=e};cu.ok=function(t){return new cu(t,null)},cu.fail=function(t){return new cu(null,t)},cu.fromReplace=function(t,e,n,r){try{return cu.ok(t.replace(e,n,r))}catch(My){if(My instanceof qc)return cu.fail(My.message);throw My}};var lu=function(t){function e(e,n,r,o){t.call(this),this.from=e,this.to=n,this.slice=r,this.structure=!!o}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.apply=function(t){return this.structure&&fu(t,this.from,this.to)?cu.fail("Structure replace would overwrite content"):cu.fromReplace(t,this.from,this.to,this.slice)},e.prototype.getMap=function(){return new tu([this.from,this.to-this.from,this.slice.size])},e.prototype.invert=function(t){return new e(this.from,this.from+this.slice.size,t.slice(this.from,this.to))},e.prototype.map=function(t){var n=t.mapResult(this.from,1),r=t.mapResult(this.to,-1);return n.deleted&&r.deleted?null:new e(n.pos,Math.max(n.pos,r.pos),this.slice)},e.prototype.merge=function(t){if(!(t instanceof e)||t.structure||this.structure)return null;if(this.from+this.slice.size!=t.from||this.slice.openEnd||t.slice.openStart){if(t.to!=this.from||this.slice.openStart||t.slice.openEnd)return null;var n=this.slice.size+t.slice.size==0?Hc.empty:new Hc(t.slice.content.append(this.slice.content),t.slice.openStart,this.slice.openEnd);return new e(t.from,this.to,n,this.structure)}var r=this.slice.size+t.slice.size==0?Hc.empty:new Hc(this.slice.content.append(t.slice.content),this.slice.openStart,t.slice.openEnd);return new e(this.from,this.to+(t.to-t.from),r,this.structure)},e.prototype.toJSON=function(){var t={stepType:"replace",from:this.from,to:this.to};return this.slice.size&&(t.slice=this.slice.toJSON()),this.structure&&(t.structure=!0),t},e.fromJSON=function(t,n){if("number"!=typeof n.from||"number"!=typeof n.to)throw new RangeError("Invalid input for ReplaceStep.fromJSON");return new e(n.from,n.to,Hc.fromJSON(t,n.slice),!!n.structure)},e}(su);su.jsonID("replace",lu);var uu=function(t){function e(e,n,r,o,i,a,s){t.call(this),this.from=e,this.to=n,this.gapFrom=r,this.gapTo=o,this.slice=i,this.insert=a,this.structure=!!s}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.apply=function(t){if(this.structure&&(fu(t,this.from,this.gapFrom)||fu(t,this.gapTo,this.to)))return cu.fail("Structure gap-replace would overwrite content");var e=t.slice(this.gapFrom,this.gapTo);if(e.openStart||e.openEnd)return cu.fail("Gap is not a flat range");var n=this.slice.insertAt(this.insert,e.content);return n?cu.fromReplace(t,this.from,this.to,n):cu.fail("Content does not fit in gap")},e.prototype.getMap=function(){return new tu([this.from,this.gapFrom-this.from,this.insert,this.gapTo,this.to-this.gapTo,this.slice.size-this.insert])},e.prototype.invert=function(t){var n=this.gapTo-this.gapFrom;return new e(this.from,this.from+this.slice.size+n,this.from+this.insert,this.from+this.insert+n,t.slice(this.from,this.to).removeBetween(this.gapFrom-this.from,this.gapTo-this.from),this.gapFrom-this.from,this.structure)},e.prototype.map=function(t){var n=t.mapResult(this.from,1),r=t.mapResult(this.to,-1),o=t.map(this.gapFrom,-1),i=t.map(this.gapTo,1);return n.deleted&&r.deleted||or.pos?null:new e(n.pos,r.pos,o,i,this.slice,this.insert,this.structure)},e.prototype.toJSON=function(){var t={stepType:"replaceAround",from:this.from,to:this.to,gapFrom:this.gapFrom,gapTo:this.gapTo,insert:this.insert};return this.slice.size&&(t.slice=this.slice.toJSON()),this.structure&&(t.structure=!0),t},e.fromJSON=function(t,n){if("number"!=typeof n.from||"number"!=typeof n.to||"number"!=typeof n.gapFrom||"number"!=typeof n.gapTo||"number"!=typeof n.insert)throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON");return new e(n.from,n.to,n.gapFrom,n.gapTo,Hc.fromJSON(t,n.slice),n.insert,!!n.structure)},e}(su);function fu(t,e,n){for(var r=t.resolve(e),o=n-e,i=r.depth;o>0&&i>0&&r.indexAfter(i)==r.node(i).childCount;)i--,o--;if(o>0)for(var a=r.node(i).maybeChild(r.indexAfter(i));o>0;){if(!a||a.isLeaf)return!0;a=a.firstChild,o--}return!1}function pu(t,e,n){return(0==e||t.canReplace(e,t.childCount))&&(n==t.childCount||t.canReplace(0,n))}function du(t){for(var e=t.parent.content.cutByIndex(t.startIndex,t.endIndex),n=t.depth;;--n){var r=t.$from.node(n),o=t.$from.index(n),i=t.$to.indexAfter(n);if(ni;s--,c--){var l=o.node(s),u=o.index(s);if(l.type.spec.isolating)return!1;var f=l.content.cutByIndex(u,l.childCount),p=r&&r[c]||l;if(p!=l&&(f=f.replaceChild(0,p.type.create(p.attrs))),!l.canReplace(u+1,l.childCount)||!p.type.validContent(f))return!1}var d=o.indexAfter(i),h=r&&r[0];return o.node(i).canReplaceWith(d,d,h?h.type:o.node(i+1).type)}function gu(t,e){var n,r,o=t.resolve(e),i=o.index();return n=o.nodeBefore,r=o.nodeAfter,n&&r&&!n.isLeaf&&n.canAppend(r)&&o.parent.canReplace(i,i+1)}function yu(t,e,n){for(var r=[],o=0;oe;f--)p||n.index(f)>0?(p=!0,l=zc.from(n.node(f).copy(l)),u++):s--;for(var d=zc.empty,h=0,v=o,m=!1;v>e;v--)m||r.after(v+1)=0;r--)n=zc.from(e[r].type.create(e[r].attrs,n));var o=t.start,i=t.end;return this.step(new uu(o,i,o,i,new Hc(n,0,0),e.length,!0))},ru.prototype.setBlockType=function(t,e,n,r){var o=this;if(void 0===e&&(e=t),!n.isTextblock)throw new RangeError("Type given to setBlockType should be a textblock");var i=this.steps.length;return this.doc.nodesBetween(t,e,(function(t,e){if(t.isTextblock&&!t.hasMarkup(n,r)&&function(t,e,n){var r=t.resolve(e),o=r.index();return r.parent.canReplaceWith(o,o+1,n)}(o.doc,o.mapping.slice(i).map(e),n)){o.clearIncompatible(o.mapping.slice(i).map(e,1),n);var a=o.mapping.slice(i),s=a.map(e,1),c=a.map(e+t.nodeSize,1);return o.step(new uu(s,c,s+1,c-1,new Hc(zc.from(n.create(r,null,t.marks)),0,0),1,!0)),!1}})),this},ru.prototype.setNodeMarkup=function(t,e,n,r){var o=this.doc.nodeAt(t);if(!o)throw new RangeError("No node at given position");e||(e=o.type);var i=e.create(n,null,r||o.marks);if(o.isLeaf)return this.replaceWith(t,t+o.nodeSize,i);if(!e.validContent(o.content))throw new RangeError("Invalid content for node type "+e.name);return this.step(new uu(t,t+o.nodeSize,t+1,t+o.nodeSize-1,new Hc(zc.from(i),0,0),1,!0))},ru.prototype.split=function(t,e,n){void 0===e&&(e=1);for(var r=this.doc.resolve(t),o=zc.empty,i=zc.empty,a=r.depth,s=r.depth-e,c=e-1;a>s;a--,c--){o=zc.from(r.node(a).copy(o));var l=n&&n[c];i=zc.from(l?l.type.create(l.attrs,i):r.node(a).copy(i))}return this.step(new lu(t,t,new Hc(o.append(i),e,e),!0))},ru.prototype.join=function(t,e){void 0===e&&(e=1);var n=new lu(t-e,t+e,Hc.empty,!0);return this.step(n)};var bu=function(t){function e(e,n,r){t.call(this),this.from=e,this.to=n,this.mark=r}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.apply=function(t){var e=this,n=t.slice(this.from,this.to),r=t.resolve(this.from),o=r.node(r.sharedDepth(this.to)),i=new Hc(yu(n.content,(function(t,n){return t.isAtom&&n.type.allowsMarkType(e.mark.type)?t.mark(e.mark.addToSet(t.marks)):t}),o),n.openStart,n.openEnd);return cu.fromReplace(t,this.from,this.to,i)},e.prototype.invert=function(){return new wu(this.from,this.to,this.mark)},e.prototype.map=function(t){var n=t.mapResult(this.from,1),r=t.mapResult(this.to,-1);return n.deleted&&r.deleted||n.pos>=r.pos?null:new e(n.pos,r.pos,this.mark)},e.prototype.merge=function(t){if(t instanceof e&&t.mark.eq(this.mark)&&this.from<=t.to&&this.to>=t.from)return new e(Math.min(this.from,t.from),Math.max(this.to,t.to),this.mark)},e.prototype.toJSON=function(){return{stepType:"addMark",mark:this.mark.toJSON(),from:this.from,to:this.to}},e.fromJSON=function(t,n){if("number"!=typeof n.from||"number"!=typeof n.to)throw new RangeError("Invalid input for AddMarkStep.fromJSON");return new e(n.from,n.to,t.markFromJSON(n.mark))},e}(su);su.jsonID("addMark",bu);var wu=function(t){function e(e,n,r){t.call(this),this.from=e,this.to=n,this.mark=r}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.apply=function(t){var e=this,n=t.slice(this.from,this.to),r=new Hc(yu(n.content,(function(t){return t.mark(e.mark.removeFromSet(t.marks))})),n.openStart,n.openEnd);return cu.fromReplace(t,this.from,this.to,r)},e.prototype.invert=function(){return new bu(this.from,this.to,this.mark)},e.prototype.map=function(t){var n=t.mapResult(this.from,1),r=t.mapResult(this.to,-1);return n.deleted&&r.deleted||n.pos>=r.pos?null:new e(n.pos,r.pos,this.mark)},e.prototype.merge=function(t){if(t instanceof e&&t.mark.eq(this.mark)&&this.from<=t.to&&this.to>=t.from)return new e(Math.min(this.from,t.from),Math.max(this.to,t.to),this.mark)},e.prototype.toJSON=function(){return{stepType:"removeMark",mark:this.mark.toJSON(),from:this.from,to:this.to}},e.fromJSON=function(t,n){if("number"!=typeof n.from||"number"!=typeof n.to)throw new RangeError("Invalid input for RemoveMarkStep.fromJSON");return new e(n.from,n.to,t.markFromJSON(n.mark))},e}(su);function xu(t,e,n,r){if(void 0===n&&(n=e),void 0===r&&(r=Hc.empty),e==n&&!r.size)return null;var o=t.resolve(e),i=t.resolve(n);return Su(o,i,r)?new lu(e,n,r):new ku(o,i,r).fit()}function Su(t,e,n){return!n.openStart&&!n.openEnd&&t.start()==e.start()&&t.parent.canReplace(t.index(),e.index(),n.content)}su.jsonID("removeMark",wu),ru.prototype.addMark=function(t,e,n){var r=this,o=[],i=[],a=null,s=null;return this.doc.nodesBetween(t,e,(function(r,c,l){if(r.isInline){var u=r.marks;if(!n.isInSet(u)&&l.type.allowsMarkType(n.type)){for(var f=Math.max(c,t),p=Math.min(c+r.nodeSize,e),d=n.addToSet(u),h=0;h=0;p--)this.step(o[p]);return this},ru.prototype.replace=function(t,e,n){void 0===e&&(e=t),void 0===n&&(n=Hc.empty);var r=xu(this.doc,t,e,n);return r&&this.step(r),this},ru.prototype.replaceWith=function(t,e,n){return this.replace(t,e,new Hc(zc.from(n),0,0))},ru.prototype.delete=function(t,e){return this.replace(t,e,Hc.empty)},ru.prototype.insert=function(t,e){return this.replaceWith(t,t,e)};var ku=function(t,e,n){this.$to=e,this.$from=t,this.unplaced=n,this.frontier=[];for(var r=0;r<=t.depth;r++){var o=t.node(r);this.frontier.push({type:o.type,match:o.contentMatchAt(t.indexAfter(r))})}this.placed=zc.empty;for(var i=t.depth;i>0;i--)this.placed=zc.from(t.node(i).copy(this.placed))},_u={depth:{configurable:!0}};function Ou(t,e,n){return 0==e?t.cutByIndex(n):t.replaceChild(0,t.firstChild.copy(Ou(t.firstChild.content,e-1,n)))}function Cu(t,e,n){return 0==e?t.append(n):t.replaceChild(t.childCount-1,t.lastChild.copy(Cu(t.lastChild.content,e-1,n)))}function Mu(t,e){for(var n=0;n1&&(r=r.replaceChild(0,Du(r.firstChild,e-1,1==r.childCount?n-1:0))),e>0&&(r=t.type.contentMatch.fillBefore(r).append(r),n<=0&&(r=r.append(t.type.contentMatch.matchFragment(r).fillBefore(zc.empty,!0)))),t.copy(r)}function Tu(t,e,n,r,o){var i=t.node(e),a=o?t.indexAfter(e):t.index(e);if(a==i.childCount&&!n.compatibleContent(i.type))return null;var s=r.fillBefore(i.content,!0,a);return s&&!function(t,e,n){for(var r=n;rr){var a=o.contentMatchAt(0),s=a.fillBefore(t).append(t);t=s.append(a.matchFragment(s).fillBefore(zc.empty,!0))}return t}function Au(t,e){for(var n=[],r=Math.min(t.depth,e.depth);r>=0;r--){var o=t.start(r);if(oe.pos+(e.depth-r)||t.node(r).type.spec.isolating||e.node(r).type.spec.isolating)break;(o==e.start(r)||r==t.depth&&r==e.depth&&t.parent.inlineContent&&e.parent.inlineContent&&r&&e.start(r-1)==o-1)&&n.push(r)}return n}_u.depth.get=function(){return this.frontier.length-1},ku.prototype.fit=function(){for(;this.unplaced.size;){var t=this.findFittable();t?this.placeNodes(t):this.openMore()||this.dropNode()}var e=this.mustMoveInline(),n=this.placed.size-this.depth-this.$from.depth,r=this.$from,o=this.close(e<0?this.$to:r.doc.resolve(e));if(!o)return null;for(var i=this.placed,a=r.depth,s=o.depth;a&&s&&1==i.childCount;)i=i.firstChild.content,a--,s--;var c=new Hc(i,a,s);return e>-1?new uu(r.pos,e,this.$to.pos,this.$to.end(),c,n):c.size||r.pos!=this.$to.pos?new lu(r.pos,o.pos,c):void 0},ku.prototype.findFittable=function(){for(var t=1;t<=2;t++)for(var e=this.unplaced.openStart;e>=0;e--)for(var n=void 0,r=(e?(n=Mu(this.unplaced.content,e-1).firstChild).content:this.unplaced.content).firstChild,o=this.depth;o>=0;o--){var i=this.frontier[o],a=i.type,s=i.match,c=void 0,l=void 0;if(1==t&&(r?s.matchType(r.type)||(l=s.fillBefore(zc.from(r),!1)):a.compatibleContent(n.type)))return{sliceDepth:e,frontierDepth:o,parent:n,inject:l};if(2==t&&r&&(c=s.findWrapping(r.type)))return{sliceDepth:e,frontierDepth:o,parent:n,wrap:c};if(n&&s.matchType(n.type))break}},ku.prototype.openMore=function(){var t=this.unplaced,e=t.content,n=t.openStart,r=t.openEnd,o=Mu(e,n);return!(!o.childCount||o.firstChild.isLeaf)&&(this.unplaced=new Hc(e,n+1,Math.max(r,o.size+n>=e.size-r?n+1:0)),!0)},ku.prototype.dropNode=function(){var t=this.unplaced,e=t.content,n=t.openStart,r=t.openEnd,o=Mu(e,n);if(o.childCount<=1&&n>0){var i=e.size-n<=n+o.size;this.unplaced=new Hc(Ou(e,n-1,1),n-1,i?n-1:r)}else this.unplaced=new Hc(Ou(e,n,1),n,r)},ku.prototype.placeNodes=function(t){for(var e=t.sliceDepth,n=t.frontierDepth,r=t.parent,o=t.inject,i=t.wrap;this.depth>n;)this.closeFrontierNode();if(i)for(var a=0;a1||0==l||g.content.size)&&(d=y,f.push(Du(g.mark(h.allowedMarks(g.marks)),1==u?l:0,u==c.childCount?m:-1)))}var b=u==c.childCount;b||(m=-1),this.placed=Cu(this.placed,n,zc.from(f)),this.frontier[n].match=d,b&&m<0&&r&&r.type==this.frontier[this.depth].type&&this.frontier.length>1&&this.closeFrontierNode();for(var w=0,x=c;w1&&r==this.$to.end(--n);)++r;return r},ku.prototype.findCloseLevel=function(t){t:for(var e=Math.min(this.depth,t.depth);e>=0;e--){var n=this.frontier[e],r=n.match,o=n.type,i=e=0;s--){var c=this.frontier[s],l=c.match,u=Tu(t,s,c.type,l,!0);if(!u||u.childCount)continue t}return{depth:e,fit:a,move:i?t.doc.resolve(t.after(e+1)):t}}}},ku.prototype.close=function(t){var e=this.findCloseLevel(t);if(!e)return null;for(;this.depth>e.depth;)this.closeFrontierNode();e.fit.childCount&&(this.placed=Cu(this.placed,e.depth,e.fit)),t=e.move;for(var n=e.depth+1;n<=t.depth;n++){var r=t.node(n),o=r.type.contentMatch.fillBefore(r.content,!0,t.index(n));this.openFrontierNode(r.type,r.attrs,o)}return t},ku.prototype.openFrontierNode=function(t,e,n){var r=this.frontier[this.depth];r.match=r.match.matchType(t),this.placed=Cu(this.placed,this.depth,zc.from(t.create(e,n))),this.frontier.push({type:t,match:t.contentMatch})},ku.prototype.closeFrontierNode=function(){var t=this.frontier.pop().match.fillBefore(zc.empty,!0);t.childCount&&(this.placed=Cu(this.placed,this.frontier.length,t))},Object.defineProperties(ku.prototype,_u),ru.prototype.replaceRange=function(t,e,n){if(!n.size)return this.deleteRange(t,e);var r=this.doc.resolve(t),o=this.doc.resolve(e);if(Su(r,o,n))return this.step(new lu(t,e,n));var i=Au(r,this.doc.resolve(e));0==i[i.length-1]&&i.pop();var a=-(r.depth+1);i.unshift(a);for(var s=r.depth,c=r.pos-1;s>0;s--,c--){var l=r.node(s).type.spec;if(l.defining||l.isolating)break;i.indexOf(s)>-1?a=s:r.before(s)==c&&i.splice(1,0,-s)}for(var u=i.indexOf(a),f=[],p=n.openStart,d=n.content,h=0;;h++){var v=d.firstChild;if(f.push(v),h==n.openStart)break;d=v.content}p>0&&f[p-1].type.spec.defining&&r.node(u).type!=f[p-1].type?p-=1:p>=2&&f[p-1].isTextblock&&f[p-2].type.spec.defining&&r.node(u).type!=f[p-2].type&&(p-=2);for(var m=n.openStart;m>=0;m--){var g=(m+p+1)%(n.openStart+1),y=f[g];if(y)for(var b=0;b=0&&(this.replace(t,e,n),!(this.steps.length>_));O--){var C=i[O];C<0||(t=r.before(C),e=o.after(C))}return this},ru.prototype.replaceRangeWith=function(t,e,n){if(!n.isInline&&t==e&&this.doc.resolve(t).parent.content.size){var r=function(t,e,n){var r=t.resolve(e);if(r.parent.canReplaceWith(r.index(),r.index(),n))return e;if(0==r.parentOffset)for(var o=r.depth-1;o>=0;o--){var i=r.index(o);if(r.node(o).canReplaceWith(i,i,n))return r.before(o+1);if(i>0)return null}if(r.parentOffset==r.parent.content.size)for(var a=r.depth-1;a>=0;a--){var s=r.indexAfter(a);if(r.node(a).canReplaceWith(s,s,n))return r.after(a+1);if(s0&&(s||n.node(a-1).canReplace(n.index(a-1),r.indexAfter(a-1))))return this.delete(n.before(a),r.after(a))}for(var c=1;c<=n.depth&&c<=r.depth;c++)if(t-n.start(c)==n.depth-c&&e>n.end(c)&&r.end(c)-e!=r.depth-c)return this.delete(n.before(c),e);return this.delete(t,e)};var Eu=Object.create(null),Nu=function(t,e,n){this.ranges=n||[new Iu(t.min(e),t.max(e))],this.$anchor=t,this.$head=e},Pu={anchor:{configurable:!0},head:{configurable:!0},from:{configurable:!0},to:{configurable:!0},$from:{configurable:!0},$to:{configurable:!0},empty:{configurable:!0}};Pu.anchor.get=function(){return this.$anchor.pos},Pu.head.get=function(){return this.$head.pos},Pu.from.get=function(){return this.$from.pos},Pu.to.get=function(){return this.$to.pos},Pu.$from.get=function(){return this.ranges[0].$from},Pu.$to.get=function(){return this.ranges[0].$to},Pu.empty.get=function(){for(var t=this.ranges,e=0;e=0;o--){var i=e<0?Vu(t.node(0),t.node(o),t.before(o+1),t.index(o),e,n):Vu(t.node(0),t.node(o),t.after(o+1),t.index(o)+1,e,n);if(i)return i}},Nu.near=function(t,e){return void 0===e&&(e=1),this.findFrom(t,e)||this.findFrom(t,-e)||new Lu(t.node(0))},Nu.atStart=function(t){return Vu(t,t,0,0,1)||new Lu(t)},Nu.atEnd=function(t){return Vu(t,t,t.content.size,t.childCount,-1)||new Lu(t)},Nu.fromJSON=function(t,e){if(!e||!e.type)throw new RangeError("Invalid input for Selection.fromJSON");var n=Eu[e.type];if(!n)throw new RangeError("No selection type "+e.type+" defined");return n.fromJSON(t,e)},Nu.jsonID=function(t,e){if(t in Eu)throw new RangeError("Duplicate use of selection JSON ID "+t);return Eu[t]=e,e.prototype.jsonID=t,e},Nu.prototype.getBookmark=function(){return ju.between(this.$anchor,this.$head).getBookmark()},Object.defineProperties(Nu.prototype,Pu),Nu.prototype.visible=!0;var Iu=function(t,e){this.$from=t,this.$to=e},ju=function(t){function e(e,n){void 0===n&&(n=e),t.call(this,e,n)}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={$cursor:{configurable:!0}};return n.$cursor.get=function(){return this.$anchor.pos==this.$head.pos?this.$head:null},e.prototype.map=function(n,r){var o=n.resolve(r.map(this.head));if(!o.parent.inlineContent)return t.near(o);var i=n.resolve(r.map(this.anchor));return new e(i.parent.inlineContent?i:o,o)},e.prototype.replace=function(e,n){if(void 0===n&&(n=Hc.empty),t.prototype.replace.call(this,e,n),n==Hc.empty){var r=this.$from.marksAcross(this.$to);r&&e.ensureMarks(r)}},e.prototype.eq=function(t){return t instanceof e&&t.anchor==this.anchor&&t.head==this.head},e.prototype.getBookmark=function(){return new Ru(this.anchor,this.head)},e.prototype.toJSON=function(){return{type:"text",anchor:this.anchor,head:this.head}},e.fromJSON=function(t,n){if("number"!=typeof n.anchor||"number"!=typeof n.head)throw new RangeError("Invalid input for TextSelection.fromJSON");return new e(t.resolve(n.anchor),t.resolve(n.head))},e.create=function(t,e,n){void 0===n&&(n=e);var r=t.resolve(e);return new this(r,n==e?r:t.resolve(n))},e.between=function(n,r,o){var i=n.pos-r.pos;if(o&&!i||(o=i>=0?1:-1),!r.parent.inlineContent){var a=t.findFrom(r,o,!0)||t.findFrom(r,-o,!0);if(!a)return t.near(r,o);r=a.$head}return n.parent.inlineContent||(0==i||(n=(t.findFrom(n,-o,!0)||t.findFrom(n,o,!0)).$anchor).pos0?0:1);o>0?a=0;a+=o){var s=e.child(a);if(s.isAtom){if(!i&&zu.isSelectable(s))return zu.create(t,n-(o<0?s.nodeSize:0))}else{var c=Vu(t,s,n+o,o<0?s.childCount:0,o,i);if(c)return c}n+=s.nodeSize*o}}function Wu(t,e,n){var r=t.steps.length-1;if(!(r0},e.prototype.setStoredMarks=function(t){return this.storedMarks=t,this.updated|=2,this},e.prototype.ensureMarks=function(t){return Wc.sameSet(this.storedMarks||this.selection.$from.marks(),t)||this.setStoredMarks(t),this},e.prototype.addStoredMark=function(t){return this.ensureMarks(t.addToSet(this.storedMarks||this.selection.$head.marks()))},e.prototype.removeStoredMark=function(t){return this.ensureMarks(t.removeFromSet(this.storedMarks||this.selection.$head.marks()))},n.storedMarksSet.get=function(){return(2&this.updated)>0},e.prototype.addStep=function(e,n){t.prototype.addStep.call(this,e,n),this.updated=-3&this.updated,this.storedMarks=null},e.prototype.setTime=function(t){return this.time=t,this},e.prototype.replaceSelection=function(t){return this.selection.replace(this,t),this},e.prototype.replaceSelectionWith=function(t,e){var n=this.selection;return!1!==e&&(t=t.mark(this.storedMarks||(n.empty?n.$from.marks():n.$from.marksAcross(n.$to)||Wc.none))),n.replaceWith(this,t),this},e.prototype.deleteSelection=function(){return this.selection.replace(this),this},e.prototype.insertText=function(t,e,n){void 0===n&&(n=e);var r=this.doc.type.schema;if(null==e)return t?this.replaceSelectionWith(r.text(t),!0):this.deleteSelection();if(!t)return this.deleteRange(e,n);var o=this.storedMarks;if(!o){var i=this.doc.resolve(e);o=n==e?i.marks():i.marksAcross(this.doc.resolve(n))}return this.replaceRangeWith(e,n,r.text(t,o)),this.selection.empty||this.setSelection(Nu.near(this.selection.$to)),this},e.prototype.setMeta=function(t,e){return this.meta["string"==typeof t?t:t.key]=e,this},e.prototype.getMeta=function(t){return this.meta["string"==typeof t?t:t.key]},n.isGeneric.get=function(){for(var t in this.meta)return!1;return!0},e.prototype.scrollIntoView=function(){return this.updated|=4,this},n.scrolledIntoView.get=function(){return(4&this.updated)>0},Object.defineProperties(e.prototype,n),e}(ru);function Hu(t,e){return e&&t?t.bind(e):t}var Ju=function(t,e,n){this.name=t,this.init=Hu(e.init,n),this.apply=Hu(e.apply,n)},Ku=[new Ju("doc",{init:function(t){return t.doc||t.schema.topNodeType.createAndFill()},apply:function(t){return t.doc}}),new Ju("selection",{init:function(t,e){return t.selection||Nu.atStart(e.doc)},apply:function(t){return t.selection}}),new Ju("storedMarks",{init:function(t){return t.storedMarks||null},apply:function(t,e,n,r){return r.selection.$cursor?t.storedMarks:null}}),new Ju("scrollToSelection",{init:function(){return 0},apply:function(t,e){return t.scrolledIntoView?e+1:e}})],Yu=function(t,e){var n=this;this.schema=t,this.fields=Ku.concat(),this.plugins=[],this.pluginsByKey=Object.create(null),e&&e.forEach((function(t){if(n.pluginsByKey[t.key])throw new RangeError("Adding different instances of a keyed plugin ("+t.key+")");n.plugins.push(t),n.pluginsByKey[t.key]=t,t.spec.state&&n.fields.push(new Ju(t.key,t.spec.state,t))}))},Uu=function(t){this.config=t},Xu={schema:{configurable:!0},plugins:{configurable:!0},tr:{configurable:!0}};Xu.schema.get=function(){return this.config.schema},Xu.plugins.get=function(){return this.config.plugins},Uu.prototype.apply=function(t){return this.applyTransaction(t).state},Uu.prototype.filterTransaction=function(t,e){void 0===e&&(e=-1);for(var n=0;n-1&&Gu.splice(e,1)},Object.defineProperties(Uu.prototype,Xu);var Gu=[];function Zu(t,e,n){for(var r in t){var o=t[r];o instanceof Function?o=o.bind(e):"handleDOMEvents"==r&&(o=Zu(o,e,{})),n[r]=o}return n}var Qu=function(t){this.props={},t.props&&Zu(t.props,this,this.props),this.spec=t,this.key=t.key?t.key.key:ef("plugin")};Qu.prototype.getState=function(t){return t[this.key]};var tf=Object.create(null);function ef(t){return t in tf?t+"$"+ ++tf[t]:(tf[t]=0,t+"$")}var nf=function(t){void 0===t&&(t="key"),this.key=ef(t)};nf.prototype.get=function(t){return t.config.pluginsByKey[this.key]},nf.prototype.getState=function(t){return t[this.key]};var rf={};if("undefined"!=typeof navigator&&"undefined"!=typeof document){var of=/Edge\/(\d+)/.exec(navigator.userAgent),af=/MSIE \d/.test(navigator.userAgent),sf=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),cf=rf.ie=!!(af||sf||of);rf.ie_version=af?document.documentMode||6:sf?+sf[1]:of?+of[1]:null,rf.gecko=!cf&&/gecko\/(\d+)/i.test(navigator.userAgent),rf.gecko_version=rf.gecko&&+(/Firefox\/(\d+)/.exec(navigator.userAgent)||[0,0])[1];var lf=!cf&&/Chrome\/(\d+)/.exec(navigator.userAgent);rf.chrome=!!lf,rf.chrome_version=lf&&+lf[1],rf.safari=!cf&&/Apple Computer/.test(navigator.vendor),rf.ios=rf.safari&&(/Mobile\/\w+/.test(navigator.userAgent)||navigator.maxTouchPoints>2),rf.mac=rf.ios||/Mac/.test(navigator.platform),rf.android=/Android \d/.test(navigator.userAgent),rf.webkit="webkitFontSmoothing"in document.documentElement.style,rf.webkit_version=rf.webkit&&+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]}var uf=function(t){for(var e=0;;e++)if(!(t=t.previousSibling))return e},ff=function(t){var e=t.assignedSlot||t.parentNode;return e&&11==e.nodeType?e.host:e},pf=null,df=function(t,e,n){var r=pf||(pf=document.createRange());return r.setEnd(t,null==n?t.nodeValue.length:n),r.setStart(t,e||0),r},hf=function(t,e,n,r){return n&&(mf(t,e,n,r,-1)||mf(t,e,n,r,1))},vf=/^(img|br|input|textarea|hr)$/i;function mf(t,e,n,r,o){for(;;){if(t==n&&e==r)return!0;if(e==(o<0?0:gf(t))){var i=t.parentNode;if(1!=i.nodeType||yf(t)||vf.test(t.nodeName)||"false"==t.contentEditable)return!1;e=uf(t)+(o<0?0:1),t=i}else{if(1!=t.nodeType)return!1;if("false"==(t=t.childNodes[e+(o<0?-1:0)]).contentEditable)return!1;e=o<0?gf(t):0}}}function gf(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}function yf(t){for(var e,n=t;n&&!(e=n.pmViewDesc);n=n.parentNode);return e&&e.node&&e.node.isBlock&&(e.dom==t||e.contentDOM==t)}var bf=function(t){var e=t.isCollapsed;return e&&rf.chrome&&t.rangeCount&&!t.getRangeAt(0).collapsed&&(e=!1),e};function wf(t,e){var n=document.createEvent("Event");return n.initEvent("keydown",!0,!0),n.keyCode=t,n.key=n.code=e,n}function xf(t){return{left:0,right:t.documentElement.clientWidth,top:0,bottom:t.documentElement.clientHeight}}function Sf(t,e){return"number"==typeof t?t:t[e]}function kf(t){var e=t.getBoundingClientRect(),n=e.width/t.offsetWidth||1,r=e.height/t.offsetHeight||1;return{left:e.left,right:e.left+t.clientWidth*n,top:e.top,bottom:e.top+t.clientHeight*r}}function _f(t,e,n){for(var r=t.someProp("scrollThreshold")||0,o=t.someProp("scrollMargin")||5,i=t.dom.ownerDocument,a=n||t.dom;a;a=ff(a))if(1==a.nodeType){var s=a==i.body||1!=a.nodeType,c=s?xf(i):kf(a),l=0,u=0;if(e.topc.bottom-Sf(r,"bottom")&&(u=e.bottom-c.bottom+Sf(o,"bottom")),e.leftc.right-Sf(r,"right")&&(l=e.right-c.right+Sf(o,"right")),l||u)if(s)i.defaultView.scrollBy(l,u);else{var f=a.scrollLeft,p=a.scrollTop;u&&(a.scrollTop+=u),l&&(a.scrollLeft+=l);var d=a.scrollLeft-f,h=a.scrollTop-p;e={left:e.left-d,top:e.top-h,right:e.right-d,bottom:e.bottom-h}}if(s)break}}function Of(t){for(var e=[],n=t.ownerDocument;t&&(e.push({dom:t,top:t.scrollTop,left:t.scrollLeft}),t!=n);t=ff(t));return e}function Cf(t,e){for(var n=0;n=s){a=Math.max(p.bottom,a),s=Math.min(p.top,s);var d=p.left>e.left?p.left-e.left:p.right=(p.left+p.right)/2?1:0));continue}}!n&&(e.left>=p.right&&e.top>=p.top||e.left>=p.left&&e.top>=p.bottom)&&(i=l+1)}}return n&&3==n.nodeType?function(t,e){for(var n=t.nodeValue.length,r=document.createRange(),o=0;o=(i.left+i.right)/2?1:0)}}return{node:t,offset:0}}(n,r):!n||o&&1==n.nodeType?{node:t,offset:i}:Df(n,r)}function Tf(t,e){return t.left>=e.left-1&&t.left<=e.right+1&&t.top>=e.top-1&&t.top<=e.bottom+1}function $f(t,e,n){var r=t.childNodes.length;if(r&&n.tope.top&&i++}o==t.dom&&i==o.childNodes.length-1&&1==o.lastChild.nodeType&&e.top>o.lastChild.getBoundingClientRect().bottom?l=t.state.doc.content.size:0!=i&&1==o.nodeType&&"BR"==o.childNodes[i-1].nodeName||(l=function(t,e,n,r){for(var o=-1,i=e;i!=t.dom;){var a=t.docView.nearestDesc(i,!0);if(!a)return null;if(a.node.isBlock&&a.parent){var s=a.dom.getBoundingClientRect();if(s.left>r.left||s.top>r.top)o=a.posBefore;else{if(!(s.right-1?o:t.docView.posFromDOM(e,n)}(t,o,i,e))}null==l&&(l=function(t,e,n){var r=Df(e,n),o=r.node,i=r.offset,a=-1;if(1==o.nodeType&&!o.firstChild){var s=o.getBoundingClientRect();a=s.left!=s.right&&n.left>(s.left+s.right)/2?1:-1}return t.docView.posFromDOM(o,i,a)}(t,u,e));var v=t.docView.nearestDesc(u,!0);return{pos:l,inside:v?v.posAtStart-v.border:-1}}function Ef(t,e){var n=t.getClientRects();return n.length?n[e<0?0:n.length-1]:t.getBoundingClientRect()}var Nf=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;function Pf(t,e,n){var r=t.docView.domFromPos(e,n<0?-1:1),o=r.node,i=r.offset,a=rf.webkit||rf.gecko;if(3==o.nodeType){if(!a||!Nf.test(o.nodeValue)&&(n<0?i:i!=o.nodeValue.length)){var s=i,c=i,l=n<0?1:-1;return n<0&&!i?(c++,l=-1):n>=0&&i==o.nodeValue.length?(s--,l=1):n<0?s--:c++,If(Ef(df(o,s,c),l),l<0)}var u=Ef(df(o,i,i),n);if(rf.gecko&&i&&/\s/.test(o.nodeValue[i-1])&&i=0)}if(i&&(n<0||i==gf(o))){var v=o.childNodes[i-1],m=3==v.nodeType?df(v,gf(v)-(a?0:1)):1!=v.nodeType||"BR"==v.nodeName&&v.nextSibling?null:v;if(m)return If(Ef(m,1),!1)}if(i=0)}function If(t,e){if(0==t.width)return t;var n=e?t.left:t.right;return{top:t.top,bottom:t.bottom,left:n,right:n}}function jf(t,e){if(0==t.height)return t;var n=e?t.top:t.bottom;return{top:n,bottom:n,left:t.left,right:t.right}}function Rf(t,e,n){var r=t.state,o=t.root.activeElement;r!=e&&t.updateState(e),o!=t.dom&&t.focus();try{return n()}finally{r!=e&&t.updateState(r),o!=t.dom&&o&&o.focus()}}var zf=/[\u0590-\u08ac]/;var Ff=null,Lf=null,Bf=!1;function Vf(t,e,n){return Ff==e&&Lf==n?Bf:(Ff=e,Lf=n,Bf="up"==n||"down"==n?function(t,e,n){var r=e.selection,o="up"==n?r.$from:r.$to;return Rf(t,e,(function(){for(var e=t.docView.domFromPos(o.pos,"up"==n?-1:1).node;;){var r=t.docView.nearestDesc(e,!0);if(!r)break;if(r.node.isBlock){e=r.dom;break}e=r.dom.parentNode}for(var i=Pf(t,o.pos,1),a=e.firstChild;a;a=a.nextSibling){var s=void 0;if(1==a.nodeType)s=a.getClientRects();else{if(3!=a.nodeType)continue;s=df(a,0,a.nodeValue.length).getClientRects()}for(var c=0;cl.top+1&&("up"==n?i.top-l.top>2*(l.bottom-i.top):l.bottom-i.bottom>2*(i.bottom-l.top)))return!1}}return!0}))}(t,e,n):function(t,e,n){var r=e.selection.$head;if(!r.parent.isTextblock)return!1;var o=r.parentOffset,i=!o,a=o==r.parent.content.size,s=t.root.getSelection();return zf.test(r.parent.textContent)&&s.modify?Rf(t,e,(function(){var e=s.getRangeAt(0),o=s.focusNode,i=s.focusOffset,a=s.caretBidiLevel;s.modify("move",n,"character");var c=!(r.depth?t.docView.domAfterPos(r.before()):t.dom).contains(1==s.focusNode.nodeType?s.focusNode:s.focusNode.parentNode)||o==s.focusNode&&i==s.focusOffset;return s.removeAllRanges(),s.addRange(e),null!=a&&(s.caretBidiLevel=a),c})):"left"==n||"backward"==n?i:a}(t,e,n))}var Wf=function(t,e,n,r){this.parent=t,this.children=e,this.dom=n,n.pmViewDesc=this,this.contentDOM=r,this.dirty=0},qf={size:{configurable:!0},border:{configurable:!0},posBefore:{configurable:!0},posAtStart:{configurable:!0},posAfter:{configurable:!0},posAtEnd:{configurable:!0},contentLost:{configurable:!0},domAtom:{configurable:!0},ignoreForCoords:{configurable:!0}};Wf.prototype.matchesWidget=function(){return!1},Wf.prototype.matchesMark=function(){return!1},Wf.prototype.matchesNode=function(){return!1},Wf.prototype.matchesHack=function(t){return!1},Wf.prototype.parseRule=function(){return null},Wf.prototype.stopEvent=function(){return!1},qf.size.get=function(){for(var t=0,e=0;euf(this.contentDOM);else if(this.contentDOM&&this.contentDOM!=this.dom&&this.dom.contains(this.contentDOM))s=2&t.compareDocumentPosition(this.contentDOM);else if(this.dom.firstChild){if(0==e)for(var c=t;;c=c.parentNode){if(c==this.dom){s=!1;break}if(c.parentNode.firstChild!=c)break}if(null==s&&e==t.childNodes.length)for(var l=t;;l=l.parentNode){if(l==this.dom){s=!0;break}if(l.parentNode.lastChild!=l)break}}return(null==s?n>0:s)?this.posAtEnd:this.posAtStart},Wf.prototype.nearestDesc=function(t,e){for(var n=!0,r=t;r;r=r.parentNode){var o=this.getDesc(r);if(o&&(!e||o.node)){if(!n||!o.nodeDOM||(1==o.nodeDOM.nodeType?o.nodeDOM.contains(1==t.nodeType?t:t.parentNode):o.nodeDOM==t))return o;n=!1}}},Wf.prototype.getDesc=function(t){for(var e=t.pmViewDesc,n=e;n;n=n.parent)if(n==this)return e},Wf.prototype.posFromDOM=function(t,e,n){for(var r=t;r;r=r.parentNode){var o=this.getDesc(r);if(o)return o.localPosFromDOM(t,e,n)}return-1},Wf.prototype.descAt=function(t){for(var e=0,n=0;et||i instanceof Zf){r=t-o;break}o=a}if(r)return this.children[n].domFromPos(r-this.children[n].border,e);for(var s=void 0;n&&!(s=this.children[n-1]).size&&s instanceof Jf&&s.widget.type.side>=0;n--);if(e<=0){for(var c,l=!0;(c=n?this.children[n-1]:null)&&c.dom.parentNode!=this.contentDOM;n--,l=!1);return c&&e&&l&&!c.border&&!c.domAtom?c.domFromPos(c.size,e):{node:this.contentDOM,offset:c?uf(c.dom)+1:0}}for(var u,f=!0;(u=n=l&&e<=c-s.border&&s.node&&s.contentDOM&&this.contentDOM.contains(s.contentDOM))return s.parseRange(t,e,l);t=i;for(var u=a;u>0;u--){var f=this.children[u-1];if(f.size&&f.dom.parentNode==this.contentDOM&&!f.emptyChildAt(1)){r=uf(f.dom)+1;break}t-=f.size}-1==r&&(r=0)}if(r>-1&&(c>e||a==this.children.length-1)){e=c;for(var p=a+1;ps&&ie){var S=u;u=f,f=S}var k=document.createRange();k.setEnd(f.node,f.offset),k.setStart(u.node,u.offset),p.removeAllRanges(),p.addRange(k)}}},Wf.prototype.ignoreMutation=function(t){return!this.contentDOM&&"selection"!=t.type},qf.contentLost.get=function(){return this.contentDOM&&this.contentDOM!=this.dom&&!this.dom.contains(this.contentDOM)},Wf.prototype.markDirty=function(t,e){for(var n=0,r=0;r=n:tn){var a=n+o.border,s=i-o.border;if(t>=a&&e<=s)return this.dirty=t==n||e==i?2:1,void(t!=a||e!=s||!o.contentLost&&o.dom.parentNode==this.contentDOM?o.markDirty(t-a,e-a):o.dirty=3);o.dirty=o.dom!=o.contentDOM||o.dom.parentNode!=this.contentDOM||o.children.length?3:2}n=i}this.dirty=2},Wf.prototype.markParentsDirty=function(){for(var t=1,e=this.parent;e;e=e.parent,t++){var n=1==t?2:1;e.dirty0&&(i=fp(i,0,t,r));for(var s=0;s-1?i:null,s=i&&i.pos<0,c=new lp(this,a&&a.node);!function(t,e,n,r){var o=e.locals(t),i=0;if(0==o.length){for(var a=0;ai;)l.push(o[c++]);var y=i+v.nodeSize;if(v.isText){var b=y;c=0&&!a&&c.syncToMarks(i==n.node.childCount?Wc.none:n.node.child(i).marks,r,t),c.placeWidget(e,t,o)}),(function(e,n,a,l){var u;c.syncToMarks(e.marks,r,t),c.findNodeMatch(e,n,a,l)||s&&t.state.selection.from>o&&t.state.selection.to-1&&c.updateNodeAt(e,n,a,u,t)||c.updateNextNode(e,n,a,t,l)||c.addNode(e,n,a,t,o),o+=e.nodeSize})),c.syncToMarks(Hf,r,t),this.node.isTextblock&&c.addTextblockHacks(),c.destroyRest(),(c.changed||2==this.dirty)&&(a&&this.protectLocalComposition(t,a),tp(this.contentDOM,this.children,t),rf.ios&&function(t){if("UL"==t.nodeName||"OL"==t.nodeName){var e=t.style.cssText;t.style.cssText=e+"; list-style: square !important",window.getComputedStyle(t).listStyle,t.style.cssText=e}}(this.dom))},e.prototype.localCompositionInfo=function(t,e){var n=t.state.selection,r=n.from,o=n.to;if(!(!(t.state.selection instanceof ju)||re+this.node.content.size)){var i=t.root.getSelection(),a=function(t,e){for(;;){if(3==t.nodeType)return t;if(1==t.nodeType&&e>0){if(t.childNodes.length>e&&3==t.childNodes[e].nodeType)return t.childNodes[e];e=gf(t=t.childNodes[e-1])}else{if(!(1==t.nodeType&&e=n&&s=0&&u+e.length+s>=n)return s+u}}}return-1}(this.node.content,s,r-e,o-e);return c<0?null:{node:a,pos:c,text:s}}return{node:a,pos:-1}}}},e.prototype.protectLocalComposition=function(t,e){var n=e.node,r=e.pos,o=e.text;if(!this.getDesc(n)){for(var i=n;i.parentNode!=this.contentDOM;i=i.parentNode){for(;i.previousSibling;)i.parentNode.removeChild(i.previousSibling);for(;i.nextSibling;)i.parentNode.removeChild(i.nextSibling);i.pmViewDesc&&(i.pmViewDesc=null)}var a=new Kf(this,i,n,o);t.compositionNodes.push(a),this.children=fp(this.children,r,r+o.length,t,a)}},e.prototype.update=function(t,e,n,r){return!(3==this.dirty||!t.sameMarkup(this.node))&&(this.updateInner(t,e,n,r),!0)},e.prototype.updateInner=function(t,e,n,r){this.updateOuterDeco(e),this.node=t,this.innerDeco=n,this.contentDOM&&this.updateChildren(r,this.posAtStart),this.dirty=0},e.prototype.updateOuterDeco=function(t){if(!sp(t,this.outerDeco)){var e=1!=this.nodeDOM.nodeType,n=this.dom;this.dom=op(this.dom,this.nodeDOM,rp(this.outerDeco,this.node,e),rp(t,this.node,e)),this.dom!=n&&(n.pmViewDesc=null,this.dom.pmViewDesc=this),this.outerDeco=t}},e.prototype.selectNode=function(){this.nodeDOM.classList.add("ProseMirror-selectednode"),!this.contentDOM&&this.node.type.spec.draggable||(this.dom.draggable=!0)},e.prototype.deselectNode=function(){this.nodeDOM.classList.remove("ProseMirror-selectednode"),!this.contentDOM&&this.node.type.spec.draggable||this.dom.removeAttribute("draggable")},n.domAtom.get=function(){return this.node.isAtom},Object.defineProperties(e.prototype,n),e}(Wf);function Xf(t,e,n,r,o){return ap(r,e,t),new Uf(null,t,e,n,r,r,r,o,0)}var Gf=function(t){function e(e,n,r,o,i,a,s){t.call(this,e,n,r,o,i,null,a,s)}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={domAtom:{configurable:!0}};return e.prototype.parseRule=function(){for(var t=this.nodeDOM.parentNode;t&&t!=this.dom&&!t.pmIsDeco;)t=t.parentNode;return{skip:t||!0}},e.prototype.update=function(t,e,n,r){return!(3==this.dirty||0!=this.dirty&&!this.inParent()||!t.sameMarkup(this.node))&&(this.updateOuterDeco(e),0==this.dirty&&t.text==this.node.text||t.text==this.nodeDOM.nodeValue||(this.nodeDOM.nodeValue=t.text,r.trackWrites==this.nodeDOM&&(r.trackWrites=null)),this.node=t,this.dirty=0,!0)},e.prototype.inParent=function(){for(var t=this.parent.contentDOM,e=this.nodeDOM;e;e=e.parentNode)if(e==t)return!0;return!1},e.prototype.domFromPos=function(t){return{node:this.nodeDOM,offset:t}},e.prototype.localPosFromDOM=function(e,n,r){return e==this.nodeDOM?this.posAtStart+Math.min(n,this.node.text.length):t.prototype.localPosFromDOM.call(this,e,n,r)},e.prototype.ignoreMutation=function(t){return"characterData"!=t.type&&"selection"!=t.type},e.prototype.slice=function(t,n,r){var o=this.node.cut(t,n),i=document.createTextNode(o.text);return new e(this.parent,o,this.outerDeco,this.innerDeco,i,i,r)},e.prototype.markDirty=function(e,n){t.prototype.markDirty.call(this,e,n),this.dom==this.nodeDOM||0!=e&&n!=this.nodeDOM.nodeValue.length||(this.dirty=3)},n.domAtom.get=function(){return!1},Object.defineProperties(e.prototype,n),e}(Uf),Zf=function(t){function e(){t.apply(this,arguments)}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={domAtom:{configurable:!0},ignoreForCoords:{configurable:!0}};return e.prototype.parseRule=function(){return{ignore:!0}},e.prototype.matchesHack=function(t){return 0==this.dirty&&this.dom.nodeName==t},n.domAtom.get=function(){return!0},n.ignoreForCoords.get=function(){return"IMG"==this.dom.nodeName},Object.defineProperties(e.prototype,n),e}(Wf),Qf=function(t){function e(e,n,r,o,i,a,s,c,l,u){t.call(this,e,n,r,o,i,a,s,l,u),this.spec=c}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.update=function(e,n,r,o){if(3==this.dirty)return!1;if(this.spec.update){var i=this.spec.update(e,n,r);return i&&this.updateInner(e,n,r,o),i}return!(!this.contentDOM&&!e.isLeaf)&&t.prototype.update.call(this,e,n,r,o)},e.prototype.selectNode=function(){this.spec.selectNode?this.spec.selectNode():t.prototype.selectNode.call(this)},e.prototype.deselectNode=function(){this.spec.deselectNode?this.spec.deselectNode():t.prototype.deselectNode.call(this)},e.prototype.setSelection=function(e,n,r,o){this.spec.setSelection?this.spec.setSelection(e,n,r):t.prototype.setSelection.call(this,e,n,r,o)},e.prototype.destroy=function(){this.spec.destroy&&this.spec.destroy(),t.prototype.destroy.call(this)},e.prototype.stopEvent=function(t){return!!this.spec.stopEvent&&this.spec.stopEvent(t)},e.prototype.ignoreMutation=function(e){return this.spec.ignoreMutation?this.spec.ignoreMutation(e):t.prototype.ignoreMutation.call(this,e)},e}(Uf);function tp(t,e,n){for(var r=t.firstChild,o=!1,i=0;i0;){for(var s=void 0;;)if(r){var c=n.children[r-1];if(!(c instanceof Yf)){s=c,r--;break}n=c,r=c.children.length}else{if(n==e)break t;r=n.parent.children.indexOf(n),n=n.parent}var l=s.node;if(l){if(l!=t.child(o-1))break;--o,i.set(s,o),a.push(s)}}return{index:o,matched:i,matches:a.reverse()}}(t.node.content,t)};function up(t,e){return t.type.side-e.type.side}function fp(t,e,n,r,o){for(var i=[],a=0,s=0;a=n||u<=e?i.push(c):(ln&&i.push(c.slice(n-l,c.size,r)))}return i}function pp(t,e){var n=t.root.getSelection(),r=t.state.doc;if(!n.focusNode)return null;var o=t.docView.nearestDesc(n.focusNode),i=o&&0==o.size,a=t.docView.posFromDOM(n.focusNode,n.focusOffset);if(a<0)return null;var s,c,l=r.resolve(a);if(bf(n)){for(s=l;o&&!o.node;)o=o.parent;if(o&&o.node.isAtom&&zu.isSelectable(o.node)&&o.parent&&(!o.node.isInline||!function(t,e,n){for(var r=0==e,o=e==gf(t);r||o;){if(t==n)return!0;var i=uf(t);if(!(t=t.parentNode))return!1;r=r&&0==i,o=o&&i==gf(t)}}(n.focusNode,n.focusOffset,o.dom))){var u=o.posBefore;c=new zu(a==u?l:r.resolve(u))}}else{var f=t.docView.posFromDOM(n.anchorNode,n.anchorOffset);if(f<0)return null;s=r.resolve(f)}c||(c=xp(t,s,l,"pointer"==e||t.state.selection.head>1,i=Math.min(o,t.length);r-1)a>this.index&&(this.changed=!0,this.destroyBetween(this.index,a)),this.top=this.top.children[this.index];else{var c=Yf.create(this.top,t[o],e,n);this.top.children.splice(this.index,0,c),this.top=c,this.changed=!0}this.index=0,o++}},lp.prototype.findNodeMatch=function(t,e,n,r){var o,i=-1;if(r>=this.preMatch.index&&(o=this.preMatch.matches[r-this.preMatch.index]).parent==this.top&&o.matchesNode(t,e,n))i=this.top.children.indexOf(o,this.index);else for(var a=this.index,s=Math.min(this.top.children.length,a+5);a0?r.max(o):r.min(o),a=i.parent.inlineContent?i.depth?t.doc.resolve(e>0?i.after():i.before()):null:i;return a&&Nu.findFrom(a,e)}function _p(t,e){return t.dispatch(t.state.tr.setSelection(e).scrollIntoView()),!0}function Op(t,e,n){var r=t.state.selection;if(!(r instanceof ju)){if(r instanceof zu&&r.node.isInline)return _p(t,new ju(e>0?r.$to:r.$from));var o=kp(t.state,e);return!!o&&_p(t,o)}if(!r.empty||n.indexOf("s")>-1)return!1;if(t.endOfTextblock(e>0?"right":"left")){var i=kp(t.state,e);return!!(i&&i instanceof zu)&&_p(t,i)}if(!(rf.mac&&n.indexOf("m")>-1)){var a,s=r.$head,c=s.textOffset?null:e<0?s.nodeBefore:s.nodeAfter;if(!c||c.isText)return!1;var l=e<0?s.pos-c.nodeSize:s.pos;return!!(c.isAtom||(a=t.docView.descAt(l))&&!a.contentDOM)&&(zu.isSelectable(c)?_p(t,new zu(e<0?t.state.doc.resolve(s.pos-c.nodeSize):s)):!!rf.webkit&&_p(t,new ju(t.state.doc.resolve(e<0?l:l+c.nodeSize))))}}function Cp(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}function Mp(t){var e=t.pmViewDesc;return e&&0==e.size&&(t.nextSibling||"BR"!=t.nodeName)}function Dp(t){var e=t.root.getSelection(),n=e.focusNode,r=e.focusOffset;if(n){var o,i,a=!1;for(rf.gecko&&1==n.nodeType&&r0){if(1!=n.nodeType)break;var s=n.childNodes[r-1];if(Mp(s))o=n,i=--r;else{if(3!=s.nodeType)break;r=(n=s).nodeValue.length}}else{if($p(n))break;for(var c=n.previousSibling;c&&Mp(c);)o=n.parentNode,i=uf(c),c=c.previousSibling;if(c)r=Cp(n=c);else{if((n=n.parentNode)==t.dom)break;r=0}}a?Ap(t,e,n,r):o&&Ap(t,e,o,i)}}function Tp(t){var e=t.root.getSelection(),n=e.focusNode,r=e.focusOffset;if(n){for(var o,i,a=Cp(n);;)if(r-1)return!1;if(rf.mac&&n.indexOf("m")>-1)return!1;var o=r.$from,i=r.$to;if(!o.parent.inlineContent||t.endOfTextblock(e<0?"up":"down")){var a=kp(t.state,e);if(a&&a instanceof zu)return _p(t,a)}if(!o.parent.inlineContent){var s=e<0?o:i,c=r instanceof Lu?Nu.near(s,e):Nu.findFrom(s,e);return!!c&&_p(t,c)}return!1}function Np(t,e){if(!(t.state.selection instanceof ju))return!0;var n=t.state.selection,r=n.$head,o=n.$anchor,i=n.empty;if(!r.sameParent(o))return!0;if(!i)return!1;if(t.endOfTextblock(e>0?"forward":"backward"))return!0;var a=!r.textOffset&&(e<0?r.nodeBefore:r.nodeAfter);if(a&&!a.isText){var s=t.state.tr;return e<0?s.delete(r.pos-a.nodeSize,r.pos):s.delete(r.pos,r.pos+a.nodeSize),t.dispatch(s),!0}return!1}function Pp(t,e,n){t.domObserver.stop(),e.contentEditable=n,t.domObserver.start()}function Ip(t,e){var n=e.keyCode,r=function(t){var e="";return t.ctrlKey&&(e+="c"),t.metaKey&&(e+="m"),t.altKey&&(e+="a"),t.shiftKey&&(e+="s"),e}(e);return 8==n||rf.mac&&72==n&&"c"==r?Np(t,-1)||Dp(t):46==n||rf.mac&&68==n&&"c"==r?Np(t,1)||Tp(t):13==n||27==n||(37==n?Op(t,-1,r)||Dp(t):39==n?Op(t,1,r)||Tp(t):38==n?Ep(t,-1,r)||Dp(t):40==n?function(t){if(rf.safari&&!(t.state.selection.$head.parentOffset>0)){var e=t.root.getSelection(),n=e.focusNode,r=e.focusOffset;if(n&&1==n.nodeType&&0==r&&n.firstChild&&"false"==n.firstChild.contentEditable){var o=n.firstChild;Pp(t,o,!0),setTimeout((function(){return Pp(t,o,!1)}),20)}}}(t)||Ep(t,1,r)||Tp(t):r==(rf.mac?"m":"c")&&(66==n||73==n||89==n||90==n))}function jp(t){var e=t.pmViewDesc;if(e)return e.parseRule();if("BR"==t.nodeName&&t.parentNode){if(rf.safari&&/^(ul|ol)$/i.test(t.parentNode.nodeName)){var n=document.createElement("div");return n.appendChild(document.createElement("li")),{skip:n}}if(t.parentNode.lastChild==t||rf.safari&&/^(tr|table)$/i.test(t.parentNode.nodeName))return{ignore:!0}}else if("IMG"==t.nodeName&&t.getAttribute("mark-placeholder"))return{ignore:!0}}function Rp(t,e,n,r,o){if(e<0){var i=t.lastSelectionTime>Date.now()-50?t.lastSelectionOrigin:null,a=pp(t,i);if(a&&!t.state.selection.eq(a)){var s=t.state.tr.setSelection(a);"pointer"==i?s.setMeta("pointer",!0):"key"==i&&s.scrollIntoView(),t.dispatch(s)}}else{var c=t.state.doc.resolve(e),l=c.sharedDepth(n);e=c.before(l+1),n=t.state.doc.resolve(n).after(l+1);var u=t.state.selection,f=function(t,e,n){var r=t.docView.parseRange(e,n),o=r.node,i=r.fromOffset,a=r.toOffset,s=r.from,c=r.to,l=t.root.getSelection(),u=null,f=l.anchorNode;if(f&&t.dom.contains(1==f.nodeType?f:f.parentNode)&&(u=[{node:f,offset:l.anchorOffset}],bf(l)||u.push({node:l.focusNode,offset:l.focusOffset})),rf.chrome&&8===t.lastKeyCode)for(var p=a;p>i;p--){var d=o.childNodes[p-1],h=d.pmViewDesc;if("BR"==d.nodeName&&!h){a=p;break}if(!h||h.size)break}var v=t.state.doc,m=t.someProp("domParser")||Rl.fromSchema(t.state.schema),g=v.resolve(s),y=null,b=m.parse(o,{topNode:g.parent,topMatch:g.parent.contentMatchAt(g.index()),topOpen:!0,from:i,to:a,preserveWhitespace:"pre"!=g.parent.type.whitespace||"full",editableContent:!0,findPositions:u,ruleFromNode:jp,context:g});if(u&&null!=u[0].pos){var w=u[0].pos,x=u[1]&&u[1].pos;null==x&&(x=w),y={anchor:w+s,head:x+s}}return{doc:b,sel:y,from:s,to:c}}(t,e,n);if(rf.chrome&&t.cursorWrapper&&f.sel&&f.sel.anchor==t.cursorWrapper.deco.from){var p=t.cursorWrapper.deco.type.toDOM.nextSibling,d=p&&p.nodeValue?p.nodeValue.length:1;f.sel={anchor:f.sel.anchor+d,head:f.sel.anchor+d}}var h,v,m=t.state.doc,g=m.slice(f.from,f.to);8===t.lastKeyCode&&Date.now()-100=s?i-r:0)+(c-s),s=i}else if(c=c?i-r:0)+(s-c),c=i}return{start:i,endA:s,endB:c}}(g.content,f.doc.content,f.from,h,v);if(!y){if(!(r&&u instanceof ju&&!u.empty&&u.$head.sameParent(u.$anchor))||t.composing||f.sel&&f.sel.anchor!=f.sel.head){if((rf.ios&&t.lastIOSEnter>Date.now()-225||rf.android)&&o.some((function(t){return"DIV"==t.nodeName||"P"==t.nodeName}))&&t.someProp("handleKeyDown",(function(e){return e(t,wf(13,"Enter"))})))return void(t.lastIOSEnter=0);if(f.sel){var b=zp(t,t.state.doc,f.sel);b&&!b.eq(t.state.selection)&&t.dispatch(t.state.tr.setSelection(b))}return}y={start:u.from,endA:u.to,endB:u.to}}t.domChangeCount++,t.state.selection.fromt.state.selection.from&&y.start<=t.state.selection.from+2?y.start=t.state.selection.from:y.endA=t.state.selection.to-2&&(y.endB+=t.state.selection.to-y.endA,y.endA=t.state.selection.to)),rf.ie&&rf.ie_version<=11&&y.endB==y.start+1&&y.endA==y.start&&y.start>f.from&&"  "==f.doc.textBetween(y.start-f.from-1,y.start-f.from+1)&&(y.start--,y.endA--,y.endB--);var w,x=f.doc.resolveNoCache(y.start-f.from),S=f.doc.resolveNoCache(y.endB-f.from),k=x.sameParent(S)&&x.parent.inlineContent;if((rf.ios&&t.lastIOSEnter>Date.now()-225&&(!k||o.some((function(t){return"DIV"==t.nodeName||"P"==t.nodeName})))||!k&&x.posy.start&&function(t,e,n,r,o){if(!r.parent.isTextblock||n-e<=o.pos-r.pos||Fp(r,!0,!1)n||Fp(a,!0,!1)e.content.size?null:xp(t,e.resolve(n.anchor),e.resolve(n.head))}function Fp(t,e,n){for(var r=t.depth,o=e?t.end():t.pos;r>0&&(e||t.indexAfter(r)==t.node(r).childCount);)r--,o++,e=!1;if(n)for(var i=t.node(r).maybeChild(t.indexAfter(r));i&&!i.isLeaf;)i=i.firstChild,o++;return o}function Lp(t,e){for(var n=[],r=e.content,o=e.openStart,i=e.openEnd;o>1&&i>1&&1==r.childCount&&1==r.firstChild.childCount;){o--,i--;var a=r.firstChild;n.push(a.type.name,a.attrs!=a.type.defaultAttrs?a.attrs:null),r=a.content}var s=t.someProp("clipboardSerializer")||Yl.fromSchema(t.state.schema),c=Xp(),l=c.createElement("div");l.appendChild(s.serializeFragment(r,{document:c}));for(var u,f=l.firstChild;f&&1==f.nodeType&&(u=Yp[f.nodeName.toLowerCase()]);){for(var p=u.length-1;p>=0;p--){for(var d=c.createElement(u[p]);l.firstChild;)d.appendChild(l.firstChild);l.appendChild(d),"tbody"!=u[p]&&(o++,i++)}f=l.firstChild}return f&&1==f.nodeType&&f.setAttribute("data-pm-slice",o+" "+i+" "+JSON.stringify(n)),{dom:l,text:t.someProp("clipboardTextSerializer",(function(t){return t(e)}))||e.content.textBetween(0,e.content.size,"\n\n")}}function Bp(t,e,n,r,o){var i,a,s=o.parent.type.spec.code;if(!n&&!e)return null;var c=e&&(r||s||!n);if(c){if(t.someProp("transformPastedText",(function(t){e=t(e,s||r)})),s)return e?new Hc(zc.from(t.state.schema.text(e.replace(/\r\n?/g,"\n"))),0,0):Hc.empty;var l=t.someProp("clipboardTextParser",(function(t){return t(e,o,r)}));if(l)a=l;else{var u=o.marks(),f=t.state.schema,p=Yl.fromSchema(f);i=document.createElement("div"),e.split(/(?:\r\n?|\n)+/).forEach((function(t){var e=i.appendChild(document.createElement("p"));t&&e.appendChild(p.serializeNode(f.text(t,u)))}))}}else t.someProp("transformPastedHTML",(function(t){n=t(n)})),i=function(t){var e=/^(\s*]*>)*/.exec(t);e&&(t=t.slice(e[0].length));var n,r=Xp().createElement("div"),o=/<([a-z][^>\s]+)/i.exec(t);(n=o&&Yp[o[1].toLowerCase()])&&(t=n.map((function(t){return"<"+t+">"})).join("")+t+n.map((function(t){return""})).reverse().join(""));if(r.innerHTML=t,n)for(var i=0;i=0;s-=2){var c=r.nodes[n[s]];if(!c||c.hasRequiredAttrs())break;o=zc.from(c.create(n[s+1],o)),i++,a++}return new Hc(o,i,a)}(Kp(a,+h[1],+h[2]),h[3]);else if(a=Hc.maxOpen(function(t,e){if(t.childCount<2)return t;for(var n=function(n){var r=e.node(n).contentMatchAt(e.index(n)),o=void 0,i=[];if(t.forEach((function(t){if(i){var e,n=r.findWrapping(t.type);if(!n)return i=null;if(e=i.length&&o.length&&qp(n,o,t,i[i.length-1],0))i[i.length-1]=e;else{i.length&&(i[i.length-1]=Hp(i[i.length-1],o.length));var a=Wp(t,n);i.push(a),r=r.matchType(a.type,a.attrs),o=n}}})),i)return{v:zc.from(i)}},r=e.depth;r>=0;r--){var o=n(r);if(o)return o.v}return t}(a.content,o),!0),a.openStart||a.openEnd){for(var m=0,g=0,y=a.content.firstChild;m=n;r--)t=e[r].create(null,zc.from(t));return t}function qp(t,e,n,r,o){if(o=n&&(s=e<0?a.contentMatchAt(0).fillBefore(s,t.childCount>1||i<=o).append(s):s.append(a.contentMatchAt(a.childCount).fillBefore(zc.empty,!0))),t.replaceChild(e<0?0:t.childCount-1,a.copy(s))}function Kp(t,e,n){return et.target.nodeValue.length}))?n.flushSoon():n.flush()})),this.currentSelection=new Qp,Zp&&(this.onCharData=function(t){n.queue.push({target:t.target,type:"characterData",oldValue:t.prevValue}),n.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this),this.suppressingSelectionUpdates=!1};td.prototype.flushSoon=function(){var t=this;this.flushingSoon<0&&(this.flushingSoon=window.setTimeout((function(){t.flushingSoon=-1,t.flush()}),20))},td.prototype.forceFlush=function(){this.flushingSoon>-1&&(window.clearTimeout(this.flushingSoon),this.flushingSoon=-1,this.flush())},td.prototype.start=function(){this.observer&&this.observer.observe(this.view.dom,Gp),Zp&&this.view.dom.addEventListener("DOMCharacterDataModified",this.onCharData),this.connectSelection()},td.prototype.stop=function(){var t=this;if(this.observer){var e=this.observer.takeRecords();if(e.length){for(var n=0;n-1)){var t=this.observer?this.observer.takeRecords():[];this.queue.length&&(t=this.queue.concat(t),this.queue.length=0);var e=this.view.root.getSelection(),n=!this.suppressingSelectionUpdates&&!this.currentSelection.eq(e)&&Sp(this.view)&&!this.ignoreSelectionChange(e),r=-1,o=-1,i=!1,a=[];if(this.view.editable)for(var s=0;s1){var l=a.filter((function(t){return"BR"==t.nodeName}));if(2==l.length){var u=l[0],f=l[1];u.parentNode&&u.parentNode.parentNode==f.parentNode?f.remove():u.remove()}}(r>-1||n)&&(r>-1&&(this.view.docView.markDirty(r,o),function(t){if(ed)return;ed=!0,"normal"==getComputedStyle(t.dom).whiteSpace&&console.warn("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.")}(this.view)),this.handleDOMChange(r,o,i,a),this.view.docView.dirty?this.view.updateState(this.view.state):this.currentSelection.eq(e)||hp(this.view),this.currentSelection.set(e))}},td.prototype.registerMutation=function(t,e){if(e.indexOf(t.target)>-1)return null;var n=this.view.docView.nearestDesc(t.target);if("attributes"==t.type&&(n==this.view.docView||"contenteditable"==t.attributeName||"style"==t.attributeName&&!t.oldValue&&!t.target.getAttribute("style")))return null;if(!n||n.ignoreMutation(t))return null;if("childList"==t.type){for(var r=0;ri.depth?e(t,n,i.nodeAfter,i.before(r),o,!0):e(t,n,i.node(r),i.before(r),o,!1)})))return{v:!0}},s=i.depth+1;s>0;s--){var c=a(s);if(c)return c.v}return!1}function ld(t,e,n){t.focused||t.focus();var r=t.state.tr.setSelection(e);"pointer"==n&&r.setMeta("pointer",!0),t.dispatch(r)}function ud(t,e,n,r,o){return cd(t,"handleClickOn",e,n,r)||t.someProp("handleClick",(function(n){return n(t,e,r)}))||(o?function(t,e){if(-1==e)return!1;var n,r,o=t.state.selection;o instanceof zu&&(n=o.node);for(var i=t.state.doc.resolve(e),a=i.depth+1;a>0;a--){var s=a>i.depth?i.nodeAfter:i.node(a);if(zu.isSelectable(s)){r=n&&o.$from.depth>0&&a>=o.$from.depth&&i.before(o.$from.depth+1)==o.$from.pos?i.before(o.$from.depth):i.before(a);break}}return null!=r&&(ld(t,zu.create(t.state.doc,r),"pointer"),!0)}(t,n):function(t,e){if(-1==e)return!1;var n=t.state.doc.resolve(e),r=n.nodeAfter;return!!(r&&r.isAtom&&zu.isSelectable(r))&&(ld(t,new zu(n),"pointer"),!0)}(t,n))}function fd(t,e,n,r){return cd(t,"handleDoubleClickOn",e,n,r)||t.someProp("handleDoubleClick",(function(n){return n(t,e,r)}))}function pd(t,e,n,r){return cd(t,"handleTripleClickOn",e,n,r)||t.someProp("handleTripleClick",(function(n){return n(t,e,r)}))||function(t,e,n){if(0!=n.button)return!1;var r=t.state.doc;if(-1==e)return!!r.inlineContent&&(ld(t,ju.create(r,0,r.content.size),"pointer"),!0);for(var o=r.resolve(e),i=o.depth+1;i>0;i--){var a=i>o.depth?o.nodeAfter:o.node(i),s=o.before(i);if(a.inlineContent)ld(t,ju.create(r,s+1,s+1+a.content.size),"pointer");else{if(!zu.isSelectable(a))continue;ld(t,zu.create(r,s),"pointer")}return!0}}(t,n,r)}function dd(t){return wd(t)}rd.keydown=function(t,e){if(t.shiftKey=16==e.keyCode||e.shiftKey,!md(t,e)&&(t.lastKeyCode=e.keyCode,t.lastKeyCodeTime=Date.now(),!rf.android||!rf.chrome||13!=e.keyCode))if(229!=e.keyCode&&t.domObserver.forceFlush(),!rf.ios||13!=e.keyCode||e.ctrlKey||e.altKey||e.metaKey)t.someProp("handleKeyDown",(function(n){return n(t,e)}))||Ip(t,e)?e.preventDefault():od(t,"key");else{var n=Date.now();t.lastIOSEnter=n,t.lastIOSEnterFallbackTimeout=setTimeout((function(){t.lastIOSEnter==n&&(t.someProp("handleKeyDown",(function(e){return e(t,wf(13,"Enter"))})),t.lastIOSEnter=0)}),200)}},rd.keyup=function(t,e){16==e.keyCode&&(t.shiftKey=!1)},rd.keypress=function(t,e){if(!(md(t,e)||!e.charCode||e.ctrlKey&&!e.altKey||rf.mac&&e.metaKey))if(t.someProp("handleKeyPress",(function(n){return n(t,e)})))e.preventDefault();else{var n=t.state.selection;if(!(n instanceof ju&&n.$from.sameParent(n.$to))){var r=String.fromCharCode(e.charCode);t.someProp("handleTextInput",(function(e){return e(t,n.$from.pos,n.$to.pos,r)}))||t.dispatch(t.state.tr.insertText(r).scrollIntoView()),e.preventDefault()}}};var hd=rf.mac?"metaKey":"ctrlKey";nd.mousedown=function(t,e){t.shiftKey=e.shiftKey;var n=dd(t),r=Date.now(),o="singleClick";r-t.lastClick.time<500&&function(t,e){var n=e.x-t.clientX,r=e.y-t.clientY;return n*n+r*r<100}(e,t.lastClick)&&!e[hd]&&("singleClick"==t.lastClick.type?o="doubleClick":"doubleClick"==t.lastClick.type&&(o="tripleClick")),t.lastClick={time:r,x:e.clientX,y:e.clientY,type:o};var i=t.posAtCoords(sd(e));i&&("singleClick"==o?(t.mouseDown&&t.mouseDown.done(),t.mouseDown=new vd(t,i,e,n)):("doubleClick"==o?fd:pd)(t,i.pos,i.inside,e)?e.preventDefault():od(t,"pointer"))};var vd=function(t,e,n,r){var o,i,a=this;if(this.view=t,this.startDoc=t.state.doc,this.pos=e,this.event=n,this.flushed=r,this.selectNode=n[hd],this.allowDefault=n.shiftKey,this.delayedSelectionSync=!1,e.inside>-1)o=t.state.doc.nodeAt(e.inside),i=e.inside;else{var s=t.state.doc.resolve(e.pos);o=s.parent,i=s.depth?s.before():0}this.mightDrag=null;var c=r?null:n.target,l=c?t.docView.nearestDesc(c,!0):null;this.target=l?l.dom:null;var u=t.state.selection;(0==n.button&&o.type.spec.draggable&&!1!==o.type.spec.selectable||u instanceof zu&&u.from<=i&&u.to>i)&&(this.mightDrag={node:o,pos:i,addAttr:this.target&&!this.target.draggable,setUneditable:this.target&&rf.gecko&&!this.target.hasAttribute("contentEditable")}),this.target&&this.mightDrag&&(this.mightDrag.addAttr||this.mightDrag.setUneditable)&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&(this.target.draggable=!0),this.mightDrag.setUneditable&&setTimeout((function(){a.view.mouseDown==a&&a.target.setAttribute("contentEditable","false")}),20),this.view.domObserver.start()),t.root.addEventListener("mouseup",this.up=this.up.bind(this)),t.root.addEventListener("mousemove",this.move=this.move.bind(this)),od(t,"pointer")};function md(t,e){return!!t.composing||!!(rf.safari&&Math.abs(e.timeStamp-t.compositionEndedAt)<500)&&(t.compositionEndedAt=-2e8,!0)}vd.prototype.done=function(){var t=this;this.view.root.removeEventListener("mouseup",this.up),this.view.root.removeEventListener("mousemove",this.move),this.mightDrag&&this.target&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&this.target.removeAttribute("draggable"),this.mightDrag.setUneditable&&this.target.removeAttribute("contentEditable"),this.view.domObserver.start()),this.delayedSelectionSync&&setTimeout((function(){return hp(t.view)})),this.view.mouseDown=null},vd.prototype.up=function(t){if(this.done(),this.view.dom.contains(3==t.target.nodeType?t.target.parentNode:t.target)){var e=this.pos;this.view.state.doc!=this.startDoc&&(e=this.view.posAtCoords(sd(t))),this.allowDefault||!e?od(this.view,"pointer"):ud(this.view,e.pos,e.inside,t,this.selectNode)?t.preventDefault():0==t.button&&(this.flushed||rf.safari&&this.mightDrag&&!this.mightDrag.node.isAtom||rf.chrome&&!(this.view.state.selection instanceof ju)&&Math.min(Math.abs(e.pos-this.view.state.selection.from),Math.abs(e.pos-this.view.state.selection.to))<=2)?(ld(this.view,Nu.near(this.view.state.doc.resolve(e.pos)),"pointer"),t.preventDefault()):od(this.view,"pointer")}},vd.prototype.move=function(t){!this.allowDefault&&(Math.abs(this.event.x-t.clientX)>4||Math.abs(this.event.y-t.clientY)>4)&&(this.allowDefault=!0),od(this.view,"pointer"),0==t.buttons&&this.done()},nd.touchdown=function(t){dd(t),od(t,"pointer")},nd.contextmenu=function(t){return dd(t)};var gd=rf.android?5e3:-1;function yd(t,e){clearTimeout(t.composingTimeout),e>-1&&(t.composingTimeout=setTimeout((function(){return wd(t)}),e))}function bd(t){var e;for(t.composing&&(t.composing=!1,t.compositionEndedAt=((e=document.createEvent("Event")).initEvent("event",!0,!0),e.timeStamp));t.compositionNodes.length>0;)t.compositionNodes.pop().markParentsDirty()}function wd(t,e){if(!(rf.android&&t.domObserver.flushingSoon>=0)){if(t.domObserver.forceFlush(),bd(t),e||t.docView.dirty){var n=pp(t);return n&&!n.eq(t.state.selection)?t.dispatch(t.state.tr.setSelection(n)):t.updateState(t.state),!0}return!1}}rd.compositionstart=rd.compositionupdate=function(t){if(!t.composing){t.domObserver.flush();var e=t.state,n=e.selection.$from;if(e.selection.empty&&(e.storedMarks||!n.textOffset&&n.parentOffset&&n.nodeBefore.marks.some((function(t){return!1===t.type.spec.inclusive}))))t.markCursor=t.state.storedMarks||n.marks(),wd(t,!0),t.markCursor=null;else if(wd(t),rf.gecko&&e.selection.empty&&n.parentOffset&&!n.textOffset&&n.nodeBefore.marks.length)for(var r=t.root.getSelection(),o=r.focusNode,i=r.focusOffset;o&&1==o.nodeType&&0!=i;){var a=i<0?o.lastChild:o.childNodes[i-1];if(!a)break;if(3==a.nodeType){r.collapse(a,a.nodeValue.length);break}o=a,i=-1}t.composing=!0}yd(t,gd)},rd.compositionend=function(t,e){t.composing&&(t.composing=!1,t.compositionEndedAt=e.timeStamp,yd(t,20))};var xd=rf.ie&&rf.ie_version<15||rf.ios&&rf.webkit_version<604;function Sd(t,e,n,r){var o=Bp(t,e,n,t.shiftKey,t.state.selection.$from);if(t.someProp("handlePaste",(function(e){return e(t,r,o||Hc.empty)})))return!0;if(!o)return!1;var i=function(t){return 0==t.openStart&&0==t.openEnd&&1==t.content.childCount?t.content.firstChild:null}(o),a=i?t.state.tr.replaceSelectionWith(i,t.shiftKey):t.state.tr.replaceSelection(o);return t.dispatch(a.scrollIntoView().setMeta("paste",!0).setMeta("uiEvent","paste")),!0}nd.copy=rd.cut=function(t,e){var n=t.state.selection,r="cut"==e.type;if(!n.empty){var o=xd?null:e.clipboardData,i=Lp(t,n.content()),a=i.dom,s=i.text;o?(e.preventDefault(),o.clearData(),o.setData("text/html",a.innerHTML),o.setData("text/plain",s)):function(t,e){if(t.dom.parentNode){var n=t.dom.parentNode.appendChild(document.createElement("div"));n.appendChild(e),n.style.cssText="position: fixed; left: -10000px; top: 10px";var r=getSelection(),o=document.createRange();o.selectNodeContents(e),t.dom.blur(),r.removeAllRanges(),r.addRange(o),setTimeout((function(){n.parentNode&&n.parentNode.removeChild(n),t.focus()}),50)}}(t,a),r&&t.dispatch(t.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent","cut"))}},rd.paste=function(t,e){if(!t.composing||rf.android){var n=xd?null:e.clipboardData;n&&Sd(t,n.getData("text/plain"),n.getData("text/html"),e)?e.preventDefault():function(t,e){if(t.dom.parentNode){var n=t.shiftKey||t.state.selection.$from.parent.type.spec.code,r=t.dom.parentNode.appendChild(document.createElement(n?"textarea":"div"));n||(r.contentEditable="true"),r.style.cssText="position: fixed; left: -10000px; top: 10px",r.focus(),setTimeout((function(){t.focus(),r.parentNode&&r.parentNode.removeChild(r),n?Sd(t,r.value,null,e):Sd(t,r.textContent,r.innerHTML,e)}),50)}}(t,e)}};var kd=function(t,e){this.slice=t,this.move=e},_d=rf.mac?"altKey":"ctrlKey";for(var Od in nd.dragstart=function(t,e){var n=t.mouseDown;if(n&&n.done(),e.dataTransfer){var r=t.state.selection,o=r.empty?null:t.posAtCoords(sd(e));if(o&&o.pos>=r.from&&o.pos<=(r instanceof zu?r.to-1:r.to));else if(n&&n.mightDrag)t.dispatch(t.state.tr.setSelection(zu.create(t.state.doc,n.mightDrag.pos)));else if(e.target&&1==e.target.nodeType){var i=t.docView.nearestDesc(e.target,!0);i&&i.node.type.spec.draggable&&i!=t.docView&&t.dispatch(t.state.tr.setSelection(zu.create(t.state.doc,i.posBefore)))}var a=t.state.selection.content(),s=Lp(t,a),c=s.dom,l=s.text;e.dataTransfer.clearData(),e.dataTransfer.setData(xd?"Text":"text/html",c.innerHTML),e.dataTransfer.effectAllowed="copyMove",xd||e.dataTransfer.setData("text/plain",l),t.dragging=new kd(a,!e[_d])}},nd.dragend=function(t){var e=t.dragging;window.setTimeout((function(){t.dragging==e&&(t.dragging=null)}),50)},rd.dragover=rd.dragenter=function(t,e){return e.preventDefault()},rd.drop=function(t,e){var n=t.dragging;if(t.dragging=null,e.dataTransfer){var r=t.posAtCoords(sd(e));if(r){var o=t.state.doc.resolve(r.pos);if(o){var i=n&&n.slice;i?t.someProp("transformPasted",(function(t){i=t(i)})):i=Bp(t,e.dataTransfer.getData(xd?"Text":"text/plain"),xd?null:e.dataTransfer.getData("text/html"),!1,o);var a=n&&!e[_d];if(t.someProp("handleDrop",(function(n){return n(t,e,i||Hc.empty,a)})))e.preventDefault();else if(i){e.preventDefault();var s=i?function(t,e,n){var r=t.resolve(e);if(!n.content.size)return e;for(var o=n.content,i=0;i=0;s--){var c=s==r.depth?0:r.pos<=(r.start(s+1)+r.end(s+1))/2?-1:1,l=r.index(s)+(c>0?1:0),u=r.node(s),f=!1;if(1==a)f=u.canReplace(l,l,o);else{var p=u.contentMatchAt(l).findWrapping(o.firstChild.type);f=p&&u.canReplaceWith(l,l,p[0])}if(f)return 0==c?r.pos:c<0?r.before(s+1):r.after(s+1)}return null}(t.state.doc,o.pos,i):o.pos;null==s&&(s=o.pos);var c=t.state.tr;a&&c.deleteSelection();var l=c.mapping.map(s),u=0==i.openStart&&0==i.openEnd&&1==i.content.childCount,f=c.doc;if(u?c.replaceRangeWith(l,l,i.content.firstChild):c.replaceRange(l,l,i),!c.doc.eq(f)){var p=c.doc.resolve(l);if(u&&zu.isSelectable(i.content.firstChild)&&p.nodeAfter&&p.nodeAfter.sameMarkup(i.content.firstChild))c.setSelection(new zu(p));else{var d=c.mapping.map(s);c.mapping.maps[c.mapping.maps.length-1].forEach((function(t,e,n,r){return d=r})),c.setSelection(xp(t,p,c.doc.resolve(d)))}t.focus(),t.dispatch(c.setMeta("uiEvent","drop"))}}}}}},nd.focus=function(t){t.focused||(t.domObserver.stop(),t.dom.classList.add("ProseMirror-focused"),t.domObserver.start(),t.focused=!0,setTimeout((function(){t.docView&&t.hasFocus()&&!t.domObserver.currentSelection.eq(t.root.getSelection())&&hp(t)}),20))},nd.blur=function(t,e){t.focused&&(t.domObserver.stop(),t.dom.classList.remove("ProseMirror-focused"),t.domObserver.start(),e.relatedTarget&&t.dom.contains(e.relatedTarget)&&t.domObserver.currentSelection.set({}),t.focused=!1)},nd.beforeinput=function(t,e){if(rf.chrome&&rf.android&&"deleteContentBackward"==e.inputType){t.domObserver.flushSoon();var n=t.domChangeCount;setTimeout((function(){if(t.domChangeCount==n&&(t.dom.blur(),t.focus(),!t.someProp("handleKeyDown",(function(e){return e(t,wf(8,"Backspace"))})))){var e=t.state.selection.$cursor;e&&e.pos>0&&t.dispatch(t.state.tr.delete(e.pos-1,e.pos).scrollIntoView())}}),50)}},rd)nd[Od]=rd[Od];function Cd(t,e){if(t==e)return!0;for(var n in t)if(t[n]!==e[n])return!1;for(var r in e)if(!(r in t))return!1;return!0}var Md=function(t,e){this.spec=e||Nd,this.side=this.spec.side||0,this.toDOM=t};Md.prototype.map=function(t,e,n,r){var o=t.mapResult(e.from+r,this.side<0?-1:1),i=o.pos;return o.deleted?null:new $d(i-n,i-n,this)},Md.prototype.valid=function(){return!0},Md.prototype.eq=function(t){return this==t||t instanceof Md&&(this.spec.key&&this.spec.key==t.spec.key||this.toDOM==t.toDOM&&Cd(this.spec,t.spec))},Md.prototype.destroy=function(t){this.spec.destroy&&this.spec.destroy(t)};var Dd=function(t,e){this.spec=e||Nd,this.attrs=t};Dd.prototype.map=function(t,e,n,r){var o=t.map(e.from+r,this.spec.inclusiveStart?-1:1)-n,i=t.map(e.to+r,this.spec.inclusiveEnd?1:-1)-n;return o>=i?null:new $d(o,i,this)},Dd.prototype.valid=function(t,e){return e.from=t&&(!o||o(a.spec))&&n.push(a.copy(a.from+r,a.to+r))}for(var s=0;st){var c=this.children[s]+1;this.children[s+2].findInner(t-c,e-c,n,r+c,o)}},Pd.prototype.map=function(t,e,n){return this==Id||0==t.maps.length?this:this.mapInner(t,e,0,0,n||Nd)},Pd.prototype.mapInner=function(t,e,n,r,o){for(var i,a=0;ac+i||(e>=s[a]+i?s[a+1]=-1:n>=o&&(l=r-n-(e-t))&&(s[a]+=l,s[a+1]+=l))}},l=0;l=r.content.size){u=!0;continue}var h=n.map(t[f+1]+i,-1)-o,v=r.content.findIndex(d),m=v.index,g=v.offset,y=r.maybeChild(m);if(y&&g==d&&g+y.nodeSize==h){var b=s[f+2].mapInner(n,y,p+1,t[f]+i+1,a);b!=Id?(s[f]=d,s[f+1]=h,s[f+2]=b):(s[f+1]=-2,u=!0)}else u=!0}if(u){var w=function(t,e,n,r,o,i,a){function s(t,e){for(var i=0;ia&&l.to=t){this.children[o]==t&&(n=this.children[o+2]);break}for(var i=t+1,a=i+e.content.size,s=0;si&&c.type instanceof Dd){var l=Math.max(i,c.from)-i,u=Math.min(a,c.to)-i;ln&&a.to0;)e++;t.splice(e,0,n)}function qd(t){var e=[];return t.someProp("decorations",(function(n){var r=n(t.state);r&&r!=Id&&e.push(r)})),t.cursorWrapper&&e.push(Pd.create(t.state.doc,[t.cursorWrapper.deco])),jd.from(e)}jd.prototype.map=function(t,e){var n=this.members.map((function(n){return n.map(t,e,Nd)}));return jd.from(n)},jd.prototype.forChild=function(t,e){if(e.isLeaf)return Pd.empty;for(var n=[],r=0;rr.scrollToSelection?"to selection":"preserve",u=o||!this.docView.matchesNode(t.doc,c,s);!u&&t.selection.eq(r.selection)||(i=!0);var f,p,d,h,v,m,g,y,b,w,x,S="preserve"==l&&i&&null==this.dom.style.overflowAnchor&&function(t){for(var e,n,r=t.dom.getBoundingClientRect(),o=Math.max(0,r.top),i=(r.left+r.right)/2,a=o+1;a=o-20){e=s,n=c.top;break}}}return{refDOM:e,refTop:n,stack:Of(t.dom)}}(this);if(i){this.domObserver.stop();var k=u&&(rf.ie||rf.chrome)&&!this.composing&&!r.selection.empty&&!t.selection.empty&&(h=r.selection,v=t.selection,m=Math.min(h.$anchor.sharedDepth(h.head),v.$anchor.sharedDepth(v.head)),h.$anchor.start(m)!=v.$anchor.start(m));if(u){var _=rf.chrome?this.trackWrites=this.root.getSelection().focusNode:null;!o&&this.docView.update(t.doc,c,s,this)||(this.docView.updateOuterDeco([]),this.docView.destroy(),this.docView=Xf(t.doc,c,s,this.dom,this)),_&&!this.trackWrites&&(k=!0)}k||!(this.mouseDown&&this.domObserver.currentSelection.eq(this.root.getSelection())&&(f=this,p=f.docView.domFromPos(f.state.selection.anchor,0),d=f.root.getSelection(),hf(p.node,p.offset,d.anchorNode,d.anchorOffset)))?hp(this,k):(bp(this,t.selection),this.domObserver.setCurSelection()),this.domObserver.start()}if(this.updatePluginViews(r),"reset"==l)this.dom.scrollTop=0;else if("to selection"==l){var O=this.root.getSelection().focusNode;this.someProp("handleScrollToSelection",(function(t){return t(n)}))||(t.selection instanceof zu?_f(this,this.docView.domAfterPos(t.selection.from).getBoundingClientRect(),O):_f(this,this.coordsAtPos(t.selection.head,1),O))}else S&&(y=(g=S).refDOM,b=g.refTop,w=g.stack,x=y?y.getBoundingClientRect().top:0,Cf(w,0==x?0:x-b))},Hd.prototype.destroyPluginViews=function(){for(var t;t=this.pluginViews.pop();)t.destroy&&t.destroy()},Hd.prototype.updatePluginViews=function(t){if(t&&t.plugins==this.state.plugins&&this.directPlugins==this.prevDirectPlugins)for(var e=0;e",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"',229:"Q"},th="undefined"!=typeof navigator&&/Chrome\/(\d+)/.exec(navigator.userAgent),eh="undefined"!=typeof navigator&&/Apple Computer/.test(navigator.vendor),nh="undefined"!=typeof navigator&&/Gecko\/\d+/.test(navigator.userAgent),rh="undefined"!=typeof navigator&&/Mac/.test(navigator.platform),oh="undefined"!=typeof navigator&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),ih=th&&(rh||+th[1]<57)||nh&&rh,ah=0;ah<10;ah++)Zd[48+ah]=Zd[96+ah]=String(ah);for(ah=1;ah<=24;ah++)Zd[ah+111]="F"+ah;for(ah=65;ah<=90;ah++)Zd[ah]=String.fromCharCode(ah+32),Qd[ah]=String.fromCharCode(ah);for(var sh in Zd)Qd.hasOwnProperty(sh)||(Qd[sh]=Zd[sh]);var ch="undefined"!=typeof navigator&&/Mac|iP(hone|[oa]d)/.test(navigator.platform);function lh(t){var e,n,r,o,i=t.split(/-(?!$)/),a=i[i.length-1];"Space"==a&&(a=" ");for(var s=0;s127)&&(r=Zd[n.keyCode])&&r!=o){var s=e[uh(r,n,!0)];if(s&&s(t.state,t.dispatch,t))return!0}else if(i&&n.shiftKey){var c=e[uh(o,n,!0)];if(c&&c(t.state,t.dispatch,t))return!0}return!1}}function dh(t,e){return!t.selection.empty&&(e&&e(t.tr.deleteSelection().scrollIntoView()),!0)}function hh(t,e,n){for(;t;t="start"==e?t.firstChild:t.lastChild){if(t.isTextblock)return!0;if(n&&1!=t.childCount)return!1}return!1}function vh(t){if(!t.parent.type.spec.isolating)for(var e=t.depth-1;e>=0;e--){if(t.index(e)>0)return t.doc.resolve(t.before(e+1));if(t.node(e).type.spec.isolating)break}return null}function mh(t){if(!t.parent.type.spec.isolating)for(var e=t.depth-1;e>=0;e--){var n=t.node(e);if(t.index(e)+1=0;u--)l=zc.from(r[u].create(null,l));l=zc.from(i.copy(l));var f=t.tr.step(new uu(e.pos-1,c,e.pos,c,new Hc(l,1,0),r.length,!0)),p=c+2*r.length;gu(f.doc,p)&&f.join(p),n(f.scrollIntoView())}return!0}var d=Nu.findFrom(e,1),h=d&&d.$from.blockRange(d.$to),v=h&&du(h);if(null!=v&&v>=e.depth)return n&&n(t.tr.lift(h,v).scrollIntoView()),!0;if(s&&hh(a,"start",!0)&&hh(i,"end")){for(var m=i,g=[];g.push(m),!m.isTextblock;)m=m.lastChild;for(var y=a,b=1;!y.isTextblock;y=y.firstChild)b++;if(m.canReplace(m.childCount,m.childCount,y.content)){if(n){for(var w=zc.empty,x=g.length-1;x>=0;x--)w=zc.from(g[x].copy(w));n(t.tr.step(new uu(e.pos-g.length,e.pos+a.nodeSize,e.pos+b,e.pos+a.nodeSize-b,new Hc(w,g.length,0),0,!0)).scrollIntoView())}return!0}}return!1}function wh(t){return function(e,n){for(var r=e.selection,o=t<0?r.$from:r.$to,i=o.depth;o.node(i).isInline;){if(!i)return!1;i--}return!!o.node(i).isTextblock&&(n&&n(e.tr.setSelection(ju.create(e.doc,t<0?o.start(i):o.end(i)))),!0)}}var xh=wh(-1),Sh=wh(1);function kh(t,e){return function(n,r){var o=n.selection,i=o.from,a=o.to,s=!1;return n.doc.nodesBetween(i,a,(function(r,o){if(s)return!1;if(r.isTextblock&&!r.hasMarkup(t,e))if(r.type==t)s=!0;else{var i=n.doc.resolve(o),a=i.index();s=i.parent.canReplaceWith(a,a+1,t)}})),!!s&&(r&&r(n.tr.setBlockType(i,a,t,e).scrollIntoView()),!0)}}function _h(t,e){return function(n,r){var o=n.selection,i=o.empty,a=o.$cursor,s=o.ranges;if(i&&!a||!function(t,e,n){for(var r=function(r){var o=e[r],i=o.$from,a=o.$to,s=0==i.depth&&t.type.allowsMarkType(n);if(t.nodesBetween(i.pos,a.pos,(function(t){if(s)return!1;s=t.inlineContent&&t.type.allowsMarkType(n)})),s)return{v:!0}},o=0;o0))return!1;var o=vh(r);if(!o){var i=r.blockRange(),a=i&&du(i);return null!=a&&(e&&e(t.tr.lift(i,a).scrollIntoView()),!0)}var s=o.nodeBefore;if(!s.type.spec.isolating&&bh(t,o,e))return!0;if(0==r.parent.content.size&&(hh(s,"end")||zu.isSelectable(s))){var c=xu(t.doc,r.before(),r.after(),Hc.empty);if(c.slice.size0)return!1;i=vh(o)}var a=i&&i.nodeBefore;return!(!a||!zu.isSelectable(a))&&(e&&e(t.tr.setSelection(zu.create(t.doc,i.pos-a.nodeSize)).scrollIntoView()),!0)})),Mh=Oh(dh,(function(t,e,n){var r=t.selection.$cursor;if(!r||(n?!n.endOfTextblock("forward",t):r.parentOffset1&&n.after()!=n.end(-1)){var r=n.before();if(mu(t.doc,r))return e&&e(t.tr.split(r).scrollIntoView()),!0}var o=n.blockRange(),i=o&&du(o);return null!=i&&(e&&e(t.tr.lift(o,i).scrollIntoView()),!0)}),(function(t,e){var n=t.selection,r=n.$from,o=n.$to;if(t.selection instanceof zu&&t.selection.node.isBlock)return!(!r.parentOffset||!mu(t.doc,r.pos))&&(e&&e(t.tr.split(r.pos).scrollIntoView()),!0);if(!r.parent.isBlock)return!1;if(e){var i=o.parentOffset==o.parent.content.size,a=t.tr;(t.selection instanceof ju||t.selection instanceof Lu)&&a.deleteSelection();var s=0==r.depth?null:gh(r.node(-1).contentMatchAt(r.indexAfter(-1))),c=i&&s?[{type:s}]:null,l=mu(a.doc,a.mapping.map(r.pos),1,c);if(c||l||!mu(a.doc,a.mapping.map(r.pos),1,s&&[{type:s}])||(c=[{type:s}],l=!0),l&&(a.split(a.mapping.map(r.pos),1,c),!i&&!r.parentOffset&&r.parent.type!=s)){var u=a.mapping.map(r.before()),f=a.doc.resolve(u);r.node(-1).canReplaceWith(f.index(),f.index()+1,s)&&a.setNodeMarkup(a.mapping.map(r.before()),s)}e(a.scrollIntoView())}return!0})),"Mod-Enter":yh,Backspace:Ch,"Mod-Backspace":Ch,"Shift-Backspace":Ch,Delete:Mh,"Mod-Delete":Mh,"Mod-a":function(t,e){return e&&e(t.tr.setSelection(new Lu(t.doc))),!0}},Th={"Ctrl-h":Dh.Backspace,"Alt-Backspace":Dh["Mod-Backspace"],"Ctrl-d":Dh.Delete,"Ctrl-Alt-Backspace":Dh["Mod-Delete"],"Alt-Delete":Dh["Mod-Delete"],"Alt-d":Dh["Mod-Delete"],"Ctrl-a":xh,"Ctrl-e":Sh};for(var $h in Dh)Th[$h]=Dh[$h];Dh.Home=xh,Dh.End=Sh;var Ah=("undefined"!=typeof navigator?/Mac|iP(hone|[oa]d)/.test(navigator.platform):"undefined"!=typeof os&&"darwin"==os.platform())?Th:Dh,Eh=function(t,e){var n;this.match=t,this.handler="string"==typeof e?(n=e,function(t,e,r,o){var i=n;if(e[1]){var a=e[0].lastIndexOf(e[1]);i+=e[0].slice(a+e[1].length);var s=(r+=a)-o;s>0&&(i=e[0].slice(a-s,a)+i,r=o)}return t.tr.insertText(i,r,o)}):e};function Nh(t){var e=t.rules,n=new Qu({state:{init:function(){return null},apply:function(t,e){var n=t.getMeta(this);return n||(t.selectionSet||t.docChanged?null:e)}},props:{handleTextInput:function(t,r,o,i){return Ph(t,r,o,i,e,n)},handleDOMEvents:{compositionend:function(t){setTimeout((function(){var r=t.state.selection.$cursor;r&&Ph(t,r.pos,r.pos,"",e,n)}))}}},isInputRules:!0});return n}function Ph(t,e,n,r,o,i){if(t.composing)return!1;var a=t.state,s=a.doc.resolve(e);if(s.parent.type.spec.code)return!1;for(var c=s.parent.textBetween(Math.max(0,s.parentOffset-500),s.parentOffset,null,"")+r,l=0;l=0;c--)a.step(s.steps[c].invert(s.docs[c]));if(i.text){var l=a.doc.resolve(i.from).marks();a.replaceWith(i.from,i.to,t.schema.text(i.text,l))}else a.delete(i.from,i.to);e(a)}return!0}}return!1}function jh(t,e,n,r){return new Eh(t,(function(t,o,i,a){var s=n instanceof Function?n(o):n,c=t.tr.delete(i,a),l=c.doc.resolve(i).blockRange(),u=l&&hu(l,e,s);if(!u)return null;c.wrap(l,u);var f=c.doc.resolve(i-1).nodeBefore;return f&&f.type==e&&gu(c.doc,i-1)&&(!r||r(o,f))&&c.join(i-1),c}))}function Rh(t,e,n){return new Eh(t,(function(t,r,o,i){var a=t.doc.resolve(o),s=n instanceof Function?n(r):n;return a.node(-1).canReplaceWith(a.index(-1),a.indexAfter(-1),e)?t.tr.delete(o,i).setBlockType(o,o,e,s):null}))}new Eh(/--$/,"—"),new Eh(/\.\.\.$/,"…"),new Eh(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/,"“"),new Eh(/"$/,"”"),new Eh(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/,"‘"),new Eh(/'$/,"’");var zh=["ol",0],Fh=["ul",0],Lh=["li",0],Bh={attrs:{order:{default:1}},parseDOM:[{tag:"ol",getAttrs:function(t){return{order:t.hasAttribute("start")?+t.getAttribute("start"):1}}}],toDOM:function(t){return 1==t.attrs.order?zh:["ol",{start:t.attrs.order},0]}},Vh={parseDOM:[{tag:"ul"}],toDOM:function(){return Fh}},Wh={parseDOM:[{tag:"li"}],toDOM:function(){return Lh},defining:!0};function qh(t,e){var n={};for(var r in t)n[r]=t[r];for(var o in e)n[o]=e[o];return n}function Hh(t,e,n){return t.append({ordered_list:qh(Bh,{content:"list_item+",group:n}),bullet_list:qh(Vh,{content:"list_item+",group:n}),list_item:qh(Wh,{content:e})})}function Jh(t,e){return function(n,r){var o=n.selection,i=o.$from,a=o.$to,s=i.blockRange(a),c=!1,l=s;if(!s)return!1;if(s.depth>=2&&i.node(s.depth-1).type.compatibleContent(t)&&0==s.startIndex){if(0==i.index(s.depth-1))return!1;var u=n.doc.resolve(s.start-2);l=new ll(u,u,s.depth),s.endIndex=0;a--)i=zc.from(n[a].type.create(n[a].attrs,i));t.step(new uu(e.start-(r?2:0),e.end,e.start,e.end,new Hc(i,0,0),n.length,!0));for(var s=0,c=0;c=o.depth-3;u--)c=zc.from(o.node(u).copy(c));var f=o.indexAfter(-1)-1)return!1;t.isTextblock&&0==t.content.size&&(h=e+1)})),h>-1&&d.setSelection(e.selection.constructor.near(d.doc.resolve(h))),n(d.scrollIntoView())}return!0}var v=i.pos==o.end()?s.contentMatchAt(0).defaultType:null,m=e.tr.delete(o.pos,i.pos),g=v&&[null,{type:v}];return!!mu(m.doc,o.pos,2,g)&&(n&&n(m.split(o.pos,2,g).scrollIntoView()),!0)}}function Yh(t){return function(e,n){var r=e.selection,o=r.$from,i=r.$to,a=o.blockRange(i,(function(e){return e.childCount&&e.firstChild.type==t}));return!!a&&(!n||(o.node(a.depth-1).type==t?function(t,e,n,r){var o=t.tr,i=r.end,a=r.$to.end(r.depth);is;a--)i-=o.child(a).nodeSize,r.delete(i-1,i+1);var c=r.doc.resolve(n.start),l=c.nodeAfter;if(r.mapping.map(n.end)!=n.start+c.nodeAfter.nodeSize)return!1;var u=0==n.startIndex,f=n.endIndex==o.childCount,p=c.node(-1),d=c.index(-1);if(!p.canReplace(d+(u?0:1),d+1,l.content.append(f?zc.empty:zc.from(o))))return!1;var h=c.pos,v=h+l.nodeSize;return r.step(new uu(h-(u?1:0),v+(f?1:0),h+1,v-1,new Hc((u?zc.empty:zc.from(o.copy(zc.empty))).append(f?zc.empty:zc.from(o.copy(zc.empty))),u?0:1,f?0:1),u?0:1)),e(r.scrollIntoView()),!0}(e,n,a)))}}function Uh(t){return function(e,n){var r=e.selection,o=r.$from,i=r.$to,a=o.blockRange(i,(function(e){return e.childCount&&e.firstChild.type==t}));if(!a)return!1;var s=a.startIndex;if(0==s)return!1;var c=a.parent,l=c.child(s-1);if(l.type!=t)return!1;if(n){var u=l.lastChild&&l.lastChild.type==c.type,f=zc.from(u?t.create():null),p=new Hc(zc.from(t.create(null,zc.from(c.type.create(null,f)))),u?3:1,0),d=a.start,h=a.end;n(e.tr.step(new uu(d-(u?3:1),h,d,h,p,1,!0)).scrollIntoView())}return!0}}var Xh=function(){};Xh.prototype.append=function(t){return t.length?(t=Xh.from(t),!this.length&&t||t.length<200&&this.leafAppend(t)||this.length<200&&t.leafPrepend(this)||this.appendInner(t)):this},Xh.prototype.prepend=function(t){return t.length?Xh.from(t).append(this):this},Xh.prototype.appendInner=function(t){return new Zh(this,t)},Xh.prototype.slice=function(t,e){return void 0===t&&(t=0),void 0===e&&(e=this.length),t>=e?Xh.empty:this.sliceInner(Math.max(0,t),Math.min(this.length,e))},Xh.prototype.get=function(t){if(!(t<0||t>=this.length))return this.getInner(t)},Xh.prototype.forEach=function(t,e,n){void 0===e&&(e=0),void 0===n&&(n=this.length),e<=n?this.forEachInner(t,e,n,0):this.forEachInvertedInner(t,e,n,0)},Xh.prototype.map=function(t,e,n){void 0===e&&(e=0),void 0===n&&(n=this.length);var r=[];return this.forEach((function(e,n){return r.push(t(e,n))}),e,n),r},Xh.from=function(t){return t instanceof Xh?t:t&&t.length?new Gh(t):Xh.empty};var Gh=function(t){function e(e){t.call(this),this.values=e}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={length:{configurable:!0},depth:{configurable:!0}};return e.prototype.flatten=function(){return this.values},e.prototype.sliceInner=function(t,n){return 0==t&&n==this.length?this:new e(this.values.slice(t,n))},e.prototype.getInner=function(t){return this.values[t]},e.prototype.forEachInner=function(t,e,n,r){for(var o=e;o=n;o--)if(!1===t(this.values[o],r+o))return!1},e.prototype.leafAppend=function(t){if(this.length+t.length<=200)return new e(this.values.concat(t.flatten()))},e.prototype.leafPrepend=function(t){if(this.length+t.length<=200)return new e(t.flatten().concat(this.values))},n.length.get=function(){return this.values.length},n.depth.get=function(){return 0},Object.defineProperties(e.prototype,n),e}(Xh);Xh.empty=new Gh([]);var Zh=function(t){function e(e,n){t.call(this),this.left=e,this.right=n,this.length=e.length+n.length,this.depth=Math.max(e.depth,n.depth)+1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.flatten=function(){return this.left.flatten().concat(this.right.flatten())},e.prototype.getInner=function(t){return to&&!1===this.right.forEachInner(t,Math.max(e-o,0),Math.min(this.length,n)-o,r+o))&&void 0)},e.prototype.forEachInvertedInner=function(t,e,n,r){var o=this.left.length;return!(e>o&&!1===this.right.forEachInvertedInner(t,e-o,Math.max(n,o)-o,r+o))&&(!(n=n?this.right.slice(t-n,e-n):this.left.slice(t,n).append(this.right.slice(0,e-n))},e.prototype.leafAppend=function(t){var n=this.right.leafAppend(t);if(n)return new e(this.left,n)},e.prototype.leafPrepend=function(t){var n=this.left.leafPrepend(t);if(n)return new e(n,this.right)},e.prototype.appendInner=function(t){return this.left.depth>=Math.max(this.right.depth,t.depth)+1?new e(this.left,new e(this.right,t)):new e(this,t)},e}(Xh),Qh=Xh,tv=function(t,e){this.items=t,this.eventCount=e};tv.prototype.popEvent=function(t,e){var n=this;if(0==this.eventCount)return null;for(var r,o,i=this.items.length;;i--){if(this.items.get(i-1).selection){--i;break}}e&&(r=this.remapping(i,this.items.length),o=r.maps.length);var a,s,c=t.tr,l=[],u=[];return this.items.forEach((function(t,e){if(!t.step)return r||(r=n.remapping(i,e+1),o=r.maps.length),o--,void u.push(t);if(r){u.push(new ev(t.map));var f,p=t.step.map(r.slice(o));p&&c.maybeStep(p).doc&&(f=c.mapping.maps[c.mapping.maps.length-1],l.push(new ev(f,null,null,l.length+u.length))),o--,f&&r.appendMap(f,o)}else c.maybeStep(t.step);return t.selection?(a=r?t.selection.map(r.slice(o)):t.selection,s=new tv(n.items.slice(0,i).append(u.reverse().concat(l)),n.eventCount-1),!1):void 0}),this.items.length,0),{remaining:s,transform:c,selection:a}},tv.prototype.addTransform=function(t,e,n,r){for(var o=[],i=this.eventCount,a=this.items,s=!r&&a.length?a.get(a.length-1):null,c=0;crv&&(d=v,(p=a).forEach((function(t,e){if(t.selection&&0==d--)return h=e,!1})),a=p.slice(h),i-=v),new tv(a.append(o),i)},tv.prototype.remapping=function(t,e){var n=new eu;return this.items.forEach((function(e,r){var o=null!=e.mirrorOffset&&r-e.mirrorOffset>=t?n.maps.length-e.mirrorOffset:null;n.appendMap(e.map,o)}),t,e),n},tv.prototype.addMaps=function(t){return 0==this.eventCount?this:new tv(this.items.append(t.map((function(t){return new ev(t)}))),this.eventCount)},tv.prototype.rebased=function(t,e){if(!this.eventCount)return this;var n=[],r=Math.max(0,this.items.length-e),o=t.mapping,i=t.steps.length,a=this.eventCount;this.items.forEach((function(t){t.selection&&a--}),r);var s=e;this.items.forEach((function(e){var r=o.getMirror(--s);if(null!=r){i=Math.min(i,r);var c=o.maps[r];if(e.step){var l=t.steps[r].invert(t.docs[r]),u=e.selection&&e.selection.map(o.slice(s+1,r));u&&a++,n.push(new ev(c,l,u))}else n.push(new ev(c))}}),r);for(var c=[],l=e;l500&&(f=f.compress(this.items.length-n.length)),f},tv.prototype.emptyItemCount=function(){var t=0;return this.items.forEach((function(e){e.step||t++})),t},tv.prototype.compress=function(t){void 0===t&&(t=this.items.length);var e=this.remapping(0,t),n=e.maps.length,r=[],o=0;return this.items.forEach((function(i,a){if(a>=t)r.push(i),i.selection&&o++;else if(i.step){var s=i.step.map(e.slice(n)),c=s&&s.getMap();if(n--,c&&e.appendMap(c,n),s){var l=i.selection&&i.selection.map(e.slice(n));l&&o++;var u,f=new ev(c.invert(),s,l),p=r.length-1;(u=r.length&&r[p].merge(f))?r[p]=u:r.push(f)}}else i.map&&n--}),this.items.length,0),new tv(Qh.from(r.reverse()),o)},tv.empty=new tv(Qh.empty,0);var ev=function(t,e,n,r){this.map=t,this.step=e,this.selection=n,this.mirrorOffset=r};ev.prototype.merge=function(t){if(this.step&&t.step&&!t.selection){var e=t.step.merge(this.step);if(e)return new ev(e.getMap().invert(),e,this.selection)}};var nv=function(t,e,n,r){this.done=t,this.undone=e,this.prevRanges=n,this.prevTime=r},rv=20;function ov(t){var e=[];return t.forEach((function(t,n,r,o){return e.push(r,o)})),e}function iv(t,e){if(!t)return null;for(var n=[],r=0;r=e[o]&&(n=!0)})),n}(n,t.prevRanges)),c=a?iv(t.prevRanges,n.mapping):ov(n.mapping.maps[n.steps.length-1]);return new nv(t.done.addTransform(n,s?e.selection.getBookmark():null,r,lv(e)),tv.empty,c,n.time)}(n,r,e,t)}},config:t,props:{handleDOMEvents:{beforeinput:function(t,e){var n="historyUndo"==e.inputType?dv(t.state,t.dispatch):"historyRedo"==e.inputType&&hv(t.state,t.dispatch);return n&&e.preventDefault(),n}}}})}function dv(t,e){var n=uv.getState(t);return!(!n||0==n.done.eventCount)&&(e&&av(n,t,e,!1),!0)}function hv(t,e){var n=uv.getState(t);return!(!n||0==n.undone.eventCount)&&(e&&av(n,t,e,!0),!0)}function vv(t){var e=uv.getState(t);return e?e.done.eventCount:0}function mv(t){var e=uv.getState(t);return e?e.undone.eventCount:0}var gv={},yv={},bv={},wv={};Object.defineProperty(wv,"__esModule",{value:!0}),wv.default=void 0;var xv=Sc.withParams;wv.default=xv,function(t){Object.defineProperty(t,"__esModule",{value:!0}),t.req=t.regex=t.ref=t.len=void 0,Object.defineProperty(t,"withParams",{enumerable:!0,get:function(){return n.default}});var e,n=(e=wv)&&e.__esModule?e:{default:e};function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var o=function(t){if(Array.isArray(t))return!!t.length;if(null==t)return!1;if(!1===t)return!0;if(t instanceof Date)return!isNaN(t.getTime());if("object"===r(t)){for(var e in t)return!0;return!1}return!!String(t).length};t.req=o;t.len=function(t){return Array.isArray(t)?t.length:"object"===r(t)?Object.keys(t).length:String(t).length};t.ref=function(t,e,n){return"function"==typeof t?t.call(e,n):n[t]};t.regex=function(t,e){return(0,n.default)({type:t},(function(t){return!o(t)||e.test(t)}))}}(bv),Object.defineProperty(yv,"__esModule",{value:!0}),yv.default=void 0;var Sv=(0,bv.regex)("alpha",/^[a-zA-Z]*$/);yv.default=Sv;var kv={};Object.defineProperty(kv,"__esModule",{value:!0}),kv.default=void 0;var _v=(0,bv.regex)("alphaNum",/^[a-zA-Z0-9]*$/);kv.default=_v;var Ov={};Object.defineProperty(Ov,"__esModule",{value:!0}),Ov.default=void 0;var Cv=(0,bv.regex)("numeric",/^[0-9]*$/);Ov.default=Cv;var Mv={};Object.defineProperty(Mv,"__esModule",{value:!0}),Mv.default=void 0;var Dv=bv;Mv.default=function(t,e){return(0,Dv.withParams)({type:"between",min:t,max:e},(function(n){return!(0,Dv.req)(n)||(!/\s/.test(n)||n instanceof Date)&&+t<=+n&&+e>=+n}))};var Tv={};Object.defineProperty(Tv,"__esModule",{value:!0}),Tv.default=void 0;var $v=(0,bv.regex)("email",/^(?:[A-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]{2,}(?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i);Tv.default=$v;var Av={};Object.defineProperty(Av,"__esModule",{value:!0}),Av.default=void 0;var Ev=bv,Nv=(0,Ev.withParams)({type:"ipAddress"},(function(t){if(!(0,Ev.req)(t))return!0;if("string"!=typeof t)return!1;var e=t.split(".");return 4===e.length&&e.every(Pv)}));Av.default=Nv;var Pv=function(t){if(t.length>3||0===t.length)return!1;if("0"===t[0]&&"0"!==t)return!1;if(!t.match(/^\d+$/))return!1;var e=0|+t;return e>=0&&e<=255},Iv={};Object.defineProperty(Iv,"__esModule",{value:!0}),Iv.default=void 0;var jv=bv;Iv.default=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:":";return(0,jv.withParams)({type:"macAddress"},(function(e){if(!(0,jv.req)(e))return!0;if("string"!=typeof e)return!1;var n="string"==typeof t&&""!==t?e.split(t):12===e.length||16===e.length?e.match(/.{2}/g):null;return null!==n&&(6===n.length||8===n.length)&&n.every(Rv)}))};var Rv=function(t){return t.toLowerCase().match(/^[0-9a-f]{2}$/)},zv={};Object.defineProperty(zv,"__esModule",{value:!0}),zv.default=void 0;var Fv=bv;zv.default=function(t){return(0,Fv.withParams)({type:"maxLength",max:t},(function(e){return!(0,Fv.req)(e)||(0,Fv.len)(e)<=t}))};var Lv={};Object.defineProperty(Lv,"__esModule",{value:!0}),Lv.default=void 0;var Bv=bv;Lv.default=function(t){return(0,Bv.withParams)({type:"minLength",min:t},(function(e){return!(0,Bv.req)(e)||(0,Bv.len)(e)>=t}))};var Vv={};Object.defineProperty(Vv,"__esModule",{value:!0}),Vv.default=void 0;var Wv=bv,qv=(0,Wv.withParams)({type:"required"},(function(t){return(0,Wv.req)("string"==typeof t?t.trim():t)}));Vv.default=qv;var Hv={};Object.defineProperty(Hv,"__esModule",{value:!0}),Hv.default=void 0;var Jv=bv;Hv.default=function(t){return(0,Jv.withParams)({type:"requiredIf",prop:t},(function(e,n){return!(0,Jv.ref)(t,this,n)||(0,Jv.req)(e)}))};var Kv={};Object.defineProperty(Kv,"__esModule",{value:!0}),Kv.default=void 0;var Yv=bv;Kv.default=function(t){return(0,Yv.withParams)({type:"requiredUnless",prop:t},(function(e,n){return!!(0,Yv.ref)(t,this,n)||(0,Yv.req)(e)}))};var Uv={};Object.defineProperty(Uv,"__esModule",{value:!0}),Uv.default=void 0;var Xv=bv;Uv.default=function(t){return(0,Xv.withParams)({type:"sameAs",eq:t},(function(e,n){return e===(0,Xv.ref)(t,this,n)}))};var Gv={};Object.defineProperty(Gv,"__esModule",{value:!0}),Gv.default=void 0;var Zv=(0,bv.regex)("url",/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i);Gv.default=Zv;var Qv={};Object.defineProperty(Qv,"__esModule",{value:!0}),Qv.default=void 0;var tm=bv;Qv.default=function(){for(var t=arguments.length,e=new Array(t),n=0;n0&&e.reduce((function(e,n){return e||n.apply(t,r)}),!1)}))};var em={};Object.defineProperty(em,"__esModule",{value:!0}),em.default=void 0;var nm=bv;em.default=function(){for(var t=arguments.length,e=new Array(t),n=0;n0&&e.reduce((function(e,n){return e&&n.apply(t,r)}),!0)}))};var rm={};Object.defineProperty(rm,"__esModule",{value:!0}),rm.default=void 0;var om=bv;rm.default=function(t){return(0,om.withParams)({type:"not"},(function(e,n){return!(0,om.req)(e)||!t.call(this,e,n)}))};var im={};Object.defineProperty(im,"__esModule",{value:!0}),im.default=void 0;var am=bv;im.default=function(t){return(0,am.withParams)({type:"minValue",min:t},(function(e){return!(0,am.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e>=+t}))};var sm={};Object.defineProperty(sm,"__esModule",{value:!0}),sm.default=void 0;var cm=bv;sm.default=function(t){return(0,cm.withParams)({type:"maxValue",max:t},(function(e){return!(0,cm.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e<=+t}))};var lm={};Object.defineProperty(lm,"__esModule",{value:!0}),lm.default=void 0;var um=(0,bv.regex)("integer",/(^[0-9]*$)|(^-[0-9]+$)/);lm.default=um;var fm={};Object.defineProperty(fm,"__esModule",{value:!0}),fm.default=void 0;var pm=(0,bv.regex)("decimal",/^[-]?\d*(\.\d+)?$/); -/**! - * Sortable 1.10.2 - * @author RubaXa - * @author owenm - * @license MIT - */ -function dm(t){return(dm="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function hm(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function vm(){return vm=Object.assign||function(t){for(var e=1;e=0||(o[n]=t[n]);return o}(t,e);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(o[n]=t[n])}return o}fm.default=pm,function(t){function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"alpha",{enumerable:!0,get:function(){return n.default}}),Object.defineProperty(t,"alphaNum",{enumerable:!0,get:function(){return r.default}}),Object.defineProperty(t,"and",{enumerable:!0,get:function(){return g.default}}),Object.defineProperty(t,"between",{enumerable:!0,get:function(){return i.default}}),Object.defineProperty(t,"decimal",{enumerable:!0,get:function(){return S.default}}),Object.defineProperty(t,"email",{enumerable:!0,get:function(){return a.default}}),t.helpers=void 0,Object.defineProperty(t,"integer",{enumerable:!0,get:function(){return x.default}}),Object.defineProperty(t,"ipAddress",{enumerable:!0,get:function(){return s.default}}),Object.defineProperty(t,"macAddress",{enumerable:!0,get:function(){return c.default}}),Object.defineProperty(t,"maxLength",{enumerable:!0,get:function(){return l.default}}),Object.defineProperty(t,"maxValue",{enumerable:!0,get:function(){return w.default}}),Object.defineProperty(t,"minLength",{enumerable:!0,get:function(){return u.default}}),Object.defineProperty(t,"minValue",{enumerable:!0,get:function(){return b.default}}),Object.defineProperty(t,"not",{enumerable:!0,get:function(){return y.default}}),Object.defineProperty(t,"numeric",{enumerable:!0,get:function(){return o.default}}),Object.defineProperty(t,"or",{enumerable:!0,get:function(){return m.default}}),Object.defineProperty(t,"required",{enumerable:!0,get:function(){return f.default}}),Object.defineProperty(t,"requiredIf",{enumerable:!0,get:function(){return p.default}}),Object.defineProperty(t,"requiredUnless",{enumerable:!0,get:function(){return d.default}}),Object.defineProperty(t,"sameAs",{enumerable:!0,get:function(){return h.default}}),Object.defineProperty(t,"url",{enumerable:!0,get:function(){return v.default}});var n=O(yv),r=O(kv),o=O(Ov),i=O(Mv),a=O(Tv),s=O(Av),c=O(Iv),l=O(zv),u=O(Lv),f=O(Vv),p=O(Hv),d=O(Kv),h=O(Uv),v=O(Gv),m=O(Qv),g=O(em),y=O(rm),b=O(im),w=O(sm),x=O(lm),S=O(fm),k=function(t,n){if(!n&&t&&t.__esModule)return t;if(null===t||"object"!==e(t)&&"function"!=typeof t)return{default:t};var r=_(n);if(r&&r.has(t))return r.get(t);var o={},i=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var a in t)if("default"!==a&&Object.prototype.hasOwnProperty.call(t,a)){var s=i?Object.getOwnPropertyDescriptor(t,a):null;s&&(s.get||s.set)?Object.defineProperty(o,a,s):o[a]=t[a]}o.default=t,r&&r.set(t,o);return o}(bv);function _(t){if("function"!=typeof WeakMap)return null;var e=new WeakMap,n=new WeakMap;return(_=function(t){return t?n:e})(t)}function O(t){return t&&t.__esModule?t:{default:t}}t.helpers=k}(gv);function ym(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var bm=ym(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),wm=ym(/Edge/i),xm=ym(/firefox/i),Sm=ym(/safari/i)&&!ym(/chrome/i)&&!ym(/android/i),km=ym(/iP(ad|od|hone)/i),_m=ym(/chrome/i)&&ym(/android/i),Om={capture:!1,passive:!1};function Cm(t,e,n){t.addEventListener(e,n,!bm&&Om)}function Mm(t,e,n){t.removeEventListener(e,n,!bm&&Om)}function Dm(t,e){if(e){if(">"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(n){return!1}return!1}}function Tm(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function $m(t,e,n,r){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&Dm(t,e):Dm(t,e))||r&&t===n)return t;if(t===n)break}while(t=Tm(t))}return null}var Am,Em=/\s+/g;function Nm(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var r=(" "+t.className+" ").replace(Em," ").replace(" "+e+" "," ");t.className=(r+(n?" "+e:"")).replace(Em," ")}}function Pm(t,e,n){var r=t&&t.style;if(r){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in r||-1!==e.indexOf("webkit")||(e="-webkit-"+e),r[e]=n+("string"==typeof n?"":"px")}}function Im(t,e){var n="";if("string"==typeof t)n=t;else do{var r=Pm(t,"transform");r&&"none"!==r&&(n=r+" "+n)}while(!e&&(t=t.parentNode));var o=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return o&&new o(n)}function jm(t,e,n){if(t){var r=t.getElementsByTagName(e),o=0,i=r.length;if(n)for(;o=i:o<=i))return r;if(r===Rm())break;r=qm(r,!1)}return!1}function Lm(t,e,n){for(var r=0,o=0,i=t.children;o2&&void 0!==arguments[2]?arguments[2]:{},r=n.evt,o=gm(n,["evt"]);Qm.pluginEvent.bind(Jg)(t,e,mm({dragEl:ng,parentEl:rg,ghostEl:og,rootEl:ig,nextEl:ag,lastDownEl:sg,cloneEl:cg,cloneHidden:lg,dragStarted:Sg,putSortable:vg,activeSortable:Jg.active,originalEvent:r,oldIndex:ug,oldDraggableIndex:pg,newIndex:fg,newDraggableIndex:dg,hideGhostForTarget:Vg,unhideGhostForTarget:Wg,cloneNowHidden:function(){lg=!0},cloneNowShown:function(){lg=!1},dispatchSortableEvent:function(t){eg({sortable:e,name:t,originalEvent:r})}},o))};function eg(t){!function(t){var e=t.sortable,n=t.rootEl,r=t.name,o=t.targetEl,i=t.cloneEl,a=t.toEl,s=t.fromEl,c=t.oldIndex,l=t.newIndex,u=t.oldDraggableIndex,f=t.newDraggableIndex,p=t.originalEvent,d=t.putSortable,h=t.extraEventProperties;if(e=e||n&&n[Um]){var v,m=e.options,g="on"+r.charAt(0).toUpperCase()+r.substr(1);!window.CustomEvent||bm||wm?(v=document.createEvent("Event")).initEvent(r,!0,!0):v=new CustomEvent(r,{bubbles:!0,cancelable:!0}),v.to=a||n,v.from=s||n,v.item=o||n,v.clone=i,v.oldIndex=c,v.newIndex=l,v.oldDraggableIndex=u,v.newDraggableIndex=f,v.originalEvent=p,v.pullMode=d?d.lastPutMode:void 0;var y=mm({},h,Qm.getEventProperties(r,e));for(var b in y)v[b]=y[b];n&&n.dispatchEvent(v),m[g]&&m[g].call(e,v)}}(mm({putSortable:vg,cloneEl:cg,targetEl:ng,rootEl:ig,oldIndex:ug,oldDraggableIndex:pg,newIndex:fg,newDraggableIndex:dg},t))}var ng,rg,og,ig,ag,sg,cg,lg,ug,fg,pg,dg,hg,vg,mg,gg,yg,bg,wg,xg,Sg,kg,_g,Og,Cg,Mg=!1,Dg=!1,Tg=[],$g=!1,Ag=!1,Eg=[],Ng=!1,Pg=[],Ig="undefined"!=typeof document,jg=km,Rg=wm||bm?"cssFloat":"float",zg=Ig&&!_m&&!km&&"draggable"in document.createElement("div"),Fg=function(){if(Ig){if(bm)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),Lg=function(t,e){var n=Pm(t),r=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),o=Lm(t,0,e),i=Lm(t,1,e),a=o&&Pm(o),s=i&&Pm(i),c=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+zm(o).width,l=s&&parseInt(s.marginLeft)+parseInt(s.marginRight)+zm(i).width;if("flex"===n.display)return"column"===n.flexDirection||"column-reverse"===n.flexDirection?"vertical":"horizontal";if("grid"===n.display)return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(o&&a.float&&"none"!==a.float){var u="left"===a.float?"left":"right";return!i||"both"!==s.clear&&s.clear!==u?"horizontal":"vertical"}return o&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||c>=r&&"none"===n[Rg]||i&&"none"===n[Rg]&&c+l>r)?"vertical":"horizontal"},Bg=function(t){function e(t,n){return function(r,o,i,a){var s=r.options.group.name&&o.options.group.name&&r.options.group.name===o.options.group.name;if(null==t&&(n||s))return!0;if(null==t||!1===t)return!1;if(n&&"clone"===t)return t;if("function"==typeof t)return e(t(r,o,i,a),n)(r,o,i,a);var c=(n?r:o).options.group.name;return!0===t||"string"==typeof t&&t===c||t.join&&t.indexOf(c)>-1}}var n={},r=t.group;r&&"object"==dm(r)||(r={name:r}),n.name=r.name,n.checkPull=e(r.pull,!0),n.checkPut=e(r.put),n.revertClone=r.revertClone,t.group=n},Vg=function(){!Fg&&og&&Pm(og,"display","none")},Wg=function(){!Fg&&og&&Pm(og,"display","")};Ig&&document.addEventListener("click",(function(t){if(Dg)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),Dg=!1,!1}),!0);var qg=function(t){if(ng){t=t.touches?t.touches[0]:t;var e=(o=t.clientX,i=t.clientY,Tg.some((function(t){if(!Bm(t)){var e=zm(t),n=t[Um].options.emptyInsertThreshold,r=o>=e.left-n&&o<=e.right+n,s=i>=e.top-n&&i<=e.bottom+n;return n&&r&&s?a=t:void 0}})),a);if(e){var n={};for(var r in t)t.hasOwnProperty(r)&&(n[r]=t[r]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[Um]._onDragOver(n)}}var o,i,a},Hg=function(t){ng&&ng.parentNode[Um]._isOutsideThisEl(t.target)};function Jg(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=vm({},e),t[Um]=this;var n={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Lg(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==Jg.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var r in Qm.initializePlugins(this,t,n),n)!(r in e)&&(e[r]=n[r]);for(var o in Bg(e),this)"_"===o.charAt(0)&&"function"==typeof this[o]&&(this[o]=this[o].bind(this));this.nativeDraggable=!e.forceFallback&&zg,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?Cm(t,"pointerdown",this._onTapStart):(Cm(t,"mousedown",this._onTapStart),Cm(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(Cm(t,"dragover",this),Cm(t,"dragenter",this)),Tg.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),vm(this,Xm())}function Kg(t,e,n,r,o,i,a,s){var c,l,u=t[Um],f=u.options.onMove;return!window.CustomEvent||bm||wm?(c=document.createEvent("Event")).initEvent("move",!0,!0):c=new CustomEvent("move",{bubbles:!0,cancelable:!0}),c.to=e,c.from=t,c.dragged=n,c.draggedRect=r,c.related=o||e,c.relatedRect=i||zm(e),c.willInsertAfter=s,c.originalEvent=a,t.dispatchEvent(c),f&&(l=f.call(u,c,a)),l}function Yg(t){t.draggable=!1}function Ug(){Ng=!1}function Xg(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,r=0;n--;)r+=e.charCodeAt(n);return r.toString(36)}function Gg(t){return setTimeout(t,0)}function Zg(t){return clearTimeout(t)}Jg.prototype={constructor:Jg,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(kg=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,ng):this.options.direction},_onTapStart:function(t){if(t.cancelable){var e=this,n=this.el,r=this.options,o=r.preventOnFilter,i=t.type,a=t.touches&&t.touches[0]||t.pointerType&&"touch"===t.pointerType&&t,s=(a||t).target,c=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||s,l=r.filter;if(function(t){Pg.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var r=e[n];r.checked&&Pg.push(r)}}(n),!ng&&!(/mousedown|pointerdown/.test(i)&&0!==t.button||r.disabled||c.isContentEditable||(s=$m(s,r.draggable,n,!1))&&s.animated||sg===s)){if(ug=Vm(s),pg=Vm(s,r.draggable),"function"==typeof l){if(l.call(this,t,s,this))return eg({sortable:e,rootEl:c,name:"filter",targetEl:s,toEl:n,fromEl:n}),tg("filter",e,{evt:t}),void(o&&t.cancelable&&t.preventDefault())}else if(l&&(l=l.split(",").some((function(r){if(r=$m(c,r.trim(),n,!1))return eg({sortable:e,rootEl:r,name:"filter",targetEl:s,fromEl:n,toEl:n}),tg("filter",e,{evt:t}),!0}))))return void(o&&t.cancelable&&t.preventDefault());r.handle&&!$m(c,r.handle,n,!1)||this._prepareDragStart(t,a,s)}}},_prepareDragStart:function(t,e,n){var r,o=this,i=o.el,a=o.options,s=i.ownerDocument;if(n&&!ng&&n.parentNode===i){var c=zm(n);if(ig=i,rg=(ng=n).parentNode,ag=ng.nextSibling,sg=n,hg=a.group,Jg.dragged=ng,mg={target:ng,clientX:(e||t).clientX,clientY:(e||t).clientY},wg=mg.clientX-c.left,xg=mg.clientY-c.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,ng.style["will-change"]="all",r=function(){tg("delayEnded",o,{evt:t}),Jg.eventCanceled?o._onDrop():(o._disableDelayedDragEvents(),!xm&&o.nativeDraggable&&(ng.draggable=!0),o._triggerDragStart(t,e),eg({sortable:o,name:"choose",originalEvent:t}),Nm(ng,a.chosenClass,!0))},a.ignore.split(",").forEach((function(t){jm(ng,t.trim(),Yg)})),Cm(s,"dragover",qg),Cm(s,"mousemove",qg),Cm(s,"touchmove",qg),Cm(s,"mouseup",o._onDrop),Cm(s,"touchend",o._onDrop),Cm(s,"touchcancel",o._onDrop),xm&&this.nativeDraggable&&(this.options.touchStartThreshold=4,ng.draggable=!0),tg("delayStart",this,{evt:t}),!a.delay||a.delayOnTouchOnly&&!e||this.nativeDraggable&&(wm||bm))r();else{if(Jg.eventCanceled)return void this._onDrop();Cm(s,"mouseup",o._disableDelayedDrag),Cm(s,"touchend",o._disableDelayedDrag),Cm(s,"touchcancel",o._disableDelayedDrag),Cm(s,"mousemove",o._delayedDragTouchMoveHandler),Cm(s,"touchmove",o._delayedDragTouchMoveHandler),a.supportPointer&&Cm(s,"pointermove",o._delayedDragTouchMoveHandler),o._dragStartTimer=setTimeout(r,a.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){ng&&Yg(ng),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;Mm(t,"mouseup",this._disableDelayedDrag),Mm(t,"touchend",this._disableDelayedDrag),Mm(t,"touchcancel",this._disableDelayedDrag),Mm(t,"mousemove",this._delayedDragTouchMoveHandler),Mm(t,"touchmove",this._delayedDragTouchMoveHandler),Mm(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?Cm(document,"pointermove",this._onTouchMove):Cm(document,e?"touchmove":"mousemove",this._onTouchMove):(Cm(ng,"dragend",this),Cm(ig,"dragstart",this._onDragStart));try{document.selection?Gg((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(n){}},_dragStarted:function(t,e){if(Mg=!1,ig&&ng){tg("dragStarted",this,{evt:e}),this.nativeDraggable&&Cm(document,"dragover",Hg);var n=this.options;!t&&Nm(ng,n.dragClass,!1),Nm(ng,n.ghostClass,!0),Jg.active=this,t&&this._appendGhost(),eg({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(gg){this._lastX=gg.clientX,this._lastY=gg.clientY,Vg();for(var t=document.elementFromPoint(gg.clientX,gg.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(gg.clientX,gg.clientY))!==e;)e=t;if(ng.parentNode[Um]._isOutsideThisEl(t),e)do{if(e[Um]){if(e[Um]._onDragOver({clientX:gg.clientX,clientY:gg.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);Wg()}},_onTouchMove:function(t){if(mg){var e=this.options,n=e.fallbackTolerance,r=e.fallbackOffset,o=t.touches?t.touches[0]:t,i=og&&Im(og,!0),a=og&&i&&i.a,s=og&&i&&i.d,c=jg&&Cg&&Wm(Cg),l=(o.clientX-mg.clientX+r.x)/(a||1)+(c?c[0]-Eg[0]:0)/(a||1),u=(o.clientY-mg.clientY+r.y)/(s||1)+(c?c[1]-Eg[1]:0)/(s||1);if(!Jg.active&&!Mg){if(n&&Math.max(Math.abs(o.clientX-this._lastX),Math.abs(o.clientY-this._lastY))r.right+o||t.clientX<=r.right&&t.clientY>r.bottom&&t.clientX>=r.left:t.clientX>r.right&&t.clientY>r.top||t.clientX<=r.right&&t.clientY>r.bottom+o}(t,o,this)&&!v.animated){if(v===ng)return $(!1);if(v&&i===t.target&&(a=v),a&&(n=zm(a)),!1!==Kg(ig,i,ng,e,a,n,t,!!a))return T(),i.appendChild(ng),rg=i,A(),$(!0)}else if(a.parentNode===i){n=zm(a);var m,g,y,b=ng.parentNode!==i,w=!function(t,e,n){var r=n?t.left:t.top,o=n?t.right:t.bottom,i=n?t.width:t.height,a=n?e.left:e.top,s=n?e.right:e.bottom,c=n?e.width:e.height;return r===a||o===s||r+i/2===a+c/2}(ng.animated&&ng.toRect||e,a.animated&&a.toRect||n,o),x=o?"top":"left",S=Fm(a,"top","top")||Fm(ng,"top","top"),k=S?S.scrollTop:void 0;if(kg!==a&&(g=n[x],$g=!1,Ag=!w&&s.invertSwap||b),m=function(t,e,n,r,o,i,a,s){var c=r?t.clientY:t.clientX,l=r?n.height:n.width,u=r?n.top:n.left,f=r?n.bottom:n.right,p=!1;if(!a)if(s&&Ogu+l*i/2:cf-Og)return-_g}else if(c>u+l*(1-o)/2&&cf-l*i/2))return c>u+l/2?1:-1;return 0}(t,a,n,o,w?1:s.swapThreshold,null==s.invertedSwapThreshold?s.swapThreshold:s.invertedSwapThreshold,Ag,kg===a),0!==m){var _=Vm(ng);do{_-=m,y=rg.children[_]}while(y&&("none"===Pm(y,"display")||y===og))}if(0===m||y===a)return $(!1);kg=a,_g=m;var O=a.nextElementSibling,C=!1,M=Kg(ig,i,ng,e,a,n,t,C=1===m);if(!1!==M)return 1!==M&&-1!==M||(C=1===M),Ng=!0,setTimeout(Ug,30),T(),C&&!O?i.appendChild(ng):a.parentNode.insertBefore(ng,C?O:a),S&&Km(S,0,k-S.scrollTop),rg=ng.parentNode,void 0===g||Ag||(Og=Math.abs(g-zm(a)[x])),A(),$(!0)}if(i.contains(ng))return $(!1)}return!1}function D(s,c){tg(s,d,mm({evt:t,isOwner:u,axis:o?"vertical":"horizontal",revert:r,dragRect:e,targetRect:n,canSort:f,fromSortable:p,target:a,completed:$,onMove:function(n,r){return Kg(ig,i,ng,e,n,zm(n),t,r)},changed:A},c))}function T(){D("dragOverAnimationCapture"),d.captureAnimationState(),d!==p&&p.captureAnimationState()}function $(e){return D("dragOverCompleted",{insertion:e}),e&&(u?l._hideClone():l._showClone(d),d!==p&&(Nm(ng,vg?vg.options.ghostClass:l.options.ghostClass,!1),Nm(ng,s.ghostClass,!0)),vg!==d&&d!==Jg.active?vg=d:d===Jg.active&&vg&&(vg=null),p===d&&(d._ignoreWhileAnimating=a),d.animateAll((function(){D("dragOverAnimationComplete"),d._ignoreWhileAnimating=null})),d!==p&&(p.animateAll(),p._ignoreWhileAnimating=null)),(a===ng&&!ng.animated||a===i&&!a.animated)&&(kg=null),s.dragoverBubble||t.rootEl||a===document||(ng.parentNode[Um]._isOutsideThisEl(t.target),!e&&qg(t)),!s.dragoverBubble&&t.stopPropagation&&t.stopPropagation(),h=!0}function A(){fg=Vm(ng),dg=Vm(ng,s.draggable),eg({sortable:d,name:"change",toEl:i,newIndex:fg,newDraggableIndex:dg,originalEvent:t})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){Mm(document,"mousemove",this._onTouchMove),Mm(document,"touchmove",this._onTouchMove),Mm(document,"pointermove",this._onTouchMove),Mm(document,"dragover",qg),Mm(document,"mousemove",qg),Mm(document,"touchmove",qg)},_offUpEvents:function(){var t=this.el.ownerDocument;Mm(t,"mouseup",this._onDrop),Mm(t,"touchend",this._onDrop),Mm(t,"pointerup",this._onDrop),Mm(t,"touchcancel",this._onDrop),Mm(document,"selectstart",this)},_onDrop:function(t){var e=this.el,n=this.options;fg=Vm(ng),dg=Vm(ng,n.draggable),tg("drop",this,{evt:t}),rg=ng&&ng.parentNode,fg=Vm(ng),dg=Vm(ng,n.draggable),Jg.eventCanceled||(Mg=!1,Ag=!1,$g=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),Zg(this.cloneId),Zg(this._dragStartId),this.nativeDraggable&&(Mm(document,"drop",this),Mm(e,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),Sm&&Pm(document.body,"user-select",""),Pm(ng,"transform",""),t&&(Sg&&(t.cancelable&&t.preventDefault(),!n.dropBubble&&t.stopPropagation()),og&&og.parentNode&&og.parentNode.removeChild(og),(ig===rg||vg&&"clone"!==vg.lastPutMode)&&cg&&cg.parentNode&&cg.parentNode.removeChild(cg),ng&&(this.nativeDraggable&&Mm(ng,"dragend",this),Yg(ng),ng.style["will-change"]="",Sg&&!Mg&&Nm(ng,vg?vg.options.ghostClass:this.options.ghostClass,!1),Nm(ng,this.options.chosenClass,!1),eg({sortable:this,name:"unchoose",toEl:rg,newIndex:null,newDraggableIndex:null,originalEvent:t}),ig!==rg?(fg>=0&&(eg({rootEl:rg,name:"add",toEl:rg,fromEl:ig,originalEvent:t}),eg({sortable:this,name:"remove",toEl:rg,originalEvent:t}),eg({rootEl:rg,name:"sort",toEl:rg,fromEl:ig,originalEvent:t}),eg({sortable:this,name:"sort",toEl:rg,originalEvent:t})),vg&&vg.save()):fg!==ug&&fg>=0&&(eg({sortable:this,name:"update",toEl:rg,originalEvent:t}),eg({sortable:this,name:"sort",toEl:rg,originalEvent:t})),Jg.active&&(null!=fg&&-1!==fg||(fg=ug,dg=pg),eg({sortable:this,name:"end",toEl:rg,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){tg("nulling",this),ig=ng=rg=og=ag=cg=sg=lg=mg=gg=Sg=fg=dg=ug=pg=kg=_g=vg=hg=Jg.dragged=Jg.ghost=Jg.clone=Jg.active=null,Pg.forEach((function(t){t.checked=!0})),Pg.length=yg=bg=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":ng&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move");t.cancelable&&t.preventDefault()}(t));break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],n=this.el.children,r=0,o=n.length,i=this.options;rt.replace(hy,((t,e)=>e?e.toUpperCase():""))));function my(t){null!==t.parentElement&&t.parentElement.removeChild(t)}function gy(t,e,n){const r=0===n?t.children[0]:t.children[n-1].nextSibling;t.insertBefore(e,r)}function yy(t,e){this.$nextTick((()=>this.$emit(t.toLowerCase(),e)))}function by(t){return e=>{null!==this.realList&&this["onDrag"+t](e),yy.call(this,t,e)}}function wy(t){return["transition-group","TransitionGroup"].includes(t)}function xy(t,e,n){return t[n]||(e[n]?e[n]():void 0)}const Sy=["Start","Add","Remove","Update","End"],ky=["Choose","Unchoose","Sort","Filter","Clone"],_y=["Move",...Sy,...ky].map((t=>"on"+t));var Oy=null;const Cy={name:"draggable",inheritAttrs:!1,props:{options:Object,list:{type:Array,required:!1,default:null},value:{type:Array,required:!1,default:null},noTransitionOnDrag:{type:Boolean,default:!1},clone:{type:Function,default:t=>t},element:{type:String,default:"div"},tag:{type:String,default:null},move:{type:Function,default:null},componentData:{type:Object,required:!1,default:null}},data:()=>({transitionMode:!1,noneFunctionalComponentMode:!1}),render(t){const e=this.$slots.default;this.transitionMode=function(t){if(!t||1!==t.length)return!1;const[{componentOptions:e}]=t;return!!e&&wy(e.tag)}(e);const{children:n,headerOffset:r,footerOffset:o}=function(t,e,n){let r=0,o=0;const i=xy(e,n,"header");i&&(r=i.length,t=t?[...i,...t]:[...i]);const a=xy(e,n,"footer");return a&&(o=a.length,t=t?[...t,...a]:[...a]),{children:t,headerOffset:r,footerOffset:o}}(e,this.$slots,this.$scopedSlots);this.headerOffset=r,this.footerOffset=o;const i=function(t,e){let n=null;const r=(t,e)=>{n=function(t,e,n){return void 0===n||((t=t||{})[e]=n),t}(n,t,e)},o=Object.keys(t).filter((t=>"id"===t||t.startsWith("data-"))).reduce(((e,n)=>(e[n]=t[n],e)),{});if(r("attrs",o),!e)return n;const{on:i,props:a,attrs:s}=e;return r("on",i),r("props",a),Object.assign(n.attrs,s),n}(this.$attrs,this.componentData);return t(this.getTag(),i,n)},created(){null!==this.list&&null!==this.value&&dy.error("Value and list props are mutually exclusive! Please set one or another."),"div"!==this.element&&dy.warn("Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"),void 0!==this.options&&dy.warn("Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props")},mounted(){if(this.noneFunctionalComponentMode=this.getTag().toLowerCase()!==this.$el.nodeName.toLowerCase()&&!this.getIsFunctional(),this.noneFunctionalComponentMode&&this.transitionMode)throw new Error(`Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}`);const t={};Sy.forEach((e=>{t["on"+e]=by.call(this,e)})),ky.forEach((e=>{t["on"+e]=yy.bind(this,e)}));const e=Object.keys(this.$attrs).reduce(((t,e)=>(t[vy(e)]=this.$attrs[e],t)),{}),n=Object.assign({},this.options,e,t,{onMove:(t,e)=>this.onDragMove(t,e)});!("draggable"in n)&&(n.draggable=">*"),this._sortable=new Jg(this.rootContainer,n),this.computeIndexes()},beforeDestroy(){void 0!==this._sortable&&this._sortable.destroy()},computed:{rootContainer(){return this.transitionMode?this.$el.children[0]:this.$el},realList(){return this.list?this.list:this.value}},watch:{options:{handler(t){this.updateOptions(t)},deep:!0},$attrs:{handler(t){this.updateOptions(t)},deep:!0},realList(){this.computeIndexes()}},methods:{getIsFunctional(){const{fnOptions:t}=this._vnode;return t&&t.functional},getTag(){return this.tag||this.element},updateOptions(t){for(var e in t){const n=vy(e);-1===_y.indexOf(n)&&this._sortable.option(n,t[e])}},getChildrenNodes(){if(this.noneFunctionalComponentMode)return this.$children[0].$slots.default;const t=this.$slots.default;return this.transitionMode?t[0].child.$slots.default:t},computeIndexes(){this.$nextTick((()=>{this.visibleIndexes=function(t,e,n,r){if(!t)return[];const o=t.map((t=>t.elm)),i=e.length-r,a=[...e].map(((t,e)=>e>=i?o.length:o.indexOf(t)));return n?a.filter((t=>-1!==t)):a}(this.getChildrenNodes(),this.rootContainer.children,this.transitionMode,this.footerOffset)}))},getUnderlyingVm(t){const e=function(t,e){return t.map((t=>t.elm)).indexOf(e)}(this.getChildrenNodes()||[],t);if(-1===e)return null;return{index:e,element:this.realList[e]}},getUnderlyingPotencialDraggableComponent:({__vue__:t})=>t&&t.$options&&wy(t.$options._componentTag)?t.$parent:!("realList"in t)&&1===t.$children.length&&"realList"in t.$children[0]?t.$children[0]:t,emitChanges(t){this.$nextTick((()=>{this.$emit("change",t)}))},alterList(t){if(this.list)return void t(this.list);const e=[...this.value];t(e),this.$emit("input",e)},spliceList(){this.alterList((t=>t.splice(...arguments)))},updatePosition(t,e){this.alterList((n=>n.splice(e,0,n.splice(t,1)[0])))},getRelatedContextFromMoveEvent({to:t,related:e}){const n=this.getUnderlyingPotencialDraggableComponent(t);if(!n)return{component:n};const r=n.realList,o={list:r,component:n};if(t!==e&&r&&n.getUnderlyingVm){const t=n.getUnderlyingVm(e);if(t)return Object.assign(t,o)}return o},getVmIndex(t){const e=this.visibleIndexes,n=e.length;return t>n-1?n:e[t]},getComponent(){return this.$slots.default[0].componentInstance},resetTransitionData(t){if(!this.noTransitionOnDrag||!this.transitionMode)return;this.getChildrenNodes()[t].data=null;const e=this.getComponent();e.children=[],e.kept=void 0},onDragStart(t){this.context=this.getUnderlyingVm(t.item),t.item._underlying_vm_=this.clone(this.context.element),Oy=t.item},onDragAdd(t){const e=t.item._underlying_vm_;if(void 0===e)return;my(t.item);const n=this.getVmIndex(t.newIndex);this.spliceList(n,0,e),this.computeIndexes();const r={element:e,newIndex:n};this.emitChanges({added:r})},onDragRemove(t){if(gy(this.rootContainer,t.item,t.oldIndex),"clone"===t.pullMode)return void my(t.clone);const e=this.context.index;this.spliceList(e,1);const n={element:this.context.element,oldIndex:e};this.resetTransitionData(e),this.emitChanges({removed:n})},onDragUpdate(t){my(t.item),gy(t.from,t.item,t.oldIndex);const e=this.context.index,n=this.getVmIndex(t.newIndex);this.updatePosition(e,n);const r={element:this.context.element,oldIndex:e,newIndex:n};this.emitChanges({moved:r})},updateProperty(t,e){t.hasOwnProperty(e)&&(t[e]+=this.headerOffset)},computeFutureIndex(t,e){if(!t.element)return 0;const n=[...e.to.children].filter((t=>"none"!==t.style.display)),r=n.indexOf(e.related),o=t.component.getVmIndex(r);return-1!==n.indexOf(Oy)||!e.willInsertAfter?o:o+1},onDragMove(t,e){const n=this.move;if(!n||!this.realList)return!0;const r=this.getRelatedContextFromMoveEvent(t),o=this.context,i=this.computeFutureIndex(r,t);Object.assign(o,{futureIndex:i});return n(Object.assign({},t,{relatedContext:r,draggedContext:o}),e)},onDragEnd(){this.computeIndexes(),Oy=null}}};"undefined"!=typeof window&&"Vue"in window&&window.Vue.component("draggable",Cy);export{mv as A,pv as B,gv as C,Rl as D,Uu as E,zc as F,Cy as G,fc as H,Eh as I,Nc as J,zu as N,Qu as P,Hc as S,ju as T,Cn as V,qs as a,Qs as b,oc as c,nc as d,Oh as e,yh as f,jh as g,Rh as h,Hh as i,Kh as j,Uh as k,Yh as l,Hs as m,fh as n,Il as o,Nh as p,Hd as q,Yl as r,kh as s,_h as t,Ih as u,Ah as v,Jh as w,dv as x,hv as y,vv as z}; +function(t){if(Number(t.version.split(".")[0])>=2)t.mixin({beforeCreate:n});else{var e=t.prototype._init;t.prototype._init=function(t){void 0===t&&(t={}),t.init=t.init?[n].concat(t.init):n,e.call(this,t)}}function n(){var t=this.$options;t.store?this.$store="function"==typeof t.store?t.store():t.store:t.parent&&t.parent.$store&&(this.$store=t.parent.$store)}}(Fa=t)}Wa.state.get=function(){return this._vm._data.$$state},Wa.state.set=function(t){},Va.prototype.commit=function(t,e,n){var r=this,i=Ya(t,e,n),o=i.type,s=i.payload,a={type:o,payload:s},l=this._mutations[o];l&&(this._withCommit((function(){l.forEach((function(t){t(s)}))})),this._subscribers.slice().forEach((function(t){return t(a,r.state)})))},Va.prototype.dispatch=function(t,e){var n=this,r=Ya(t,e),i=r.type,o=r.payload,s={type:i,payload:o},a=this._actions[i];if(a){try{this._actionSubscribers.slice().filter((function(t){return t.before})).forEach((function(t){return t.before(s,n.state)}))}catch(_g){}var l=a.length>1?Promise.all(a.map((function(t){return t(o)}))):a[0](o);return new Promise((function(t,e){l.then((function(e){try{n._actionSubscribers.filter((function(t){return t.after})).forEach((function(t){return t.after(s,n.state)}))}catch(_g){}t(e)}),(function(t){try{n._actionSubscribers.filter((function(t){return t.error})).forEach((function(e){return e.error(s,n.state,t)}))}catch(_g){}e(t)}))}))}},Va.prototype.subscribe=function(t,e){return qa(t,this._subscribers,e)},Va.prototype.subscribeAction=function(t,e){return qa("function"==typeof t?{before:t}:t,this._actionSubscribers,e)},Va.prototype.watch=function(t,e,n){var r=this;return this._watcherVM.$watch((function(){return t(r.state,r.getters)}),e,n)},Va.prototype.replaceState=function(t){var e=this;this._withCommit((function(){e._vm._data.$$state=t}))},Va.prototype.registerModule=function(t,e,n){void 0===n&&(n={}),"string"==typeof t&&(t=[t]),this._modules.register(t,e),Ha(this,this.state,t,this._modules.get(t),n.preserveState),Ka(this,this.state)},Va.prototype.unregisterModule=function(t){var e=this;"string"==typeof t&&(t=[t]),this._modules.unregister(t),this._withCommit((function(){var n=Ua(e.state,t.slice(0,-1));Fa.delete(n,t[t.length-1])})),Ja(this)},Va.prototype.hasModule=function(t){return"string"==typeof t&&(t=[t]),this._modules.isRegistered(t)},Va.prototype.hotUpdate=function(t){this._modules.update(t),Ja(this,!0)},Va.prototype._withCommit=function(t){var e=this._committing;this._committing=!0,t(),this._committing=e},Object.defineProperties(Va.prototype,Wa);var Za=nl((function(t,e){var n={};return el(e).forEach((function(e){var r=e.key,i=e.val;n[r]=function(){var e=this.$store.state,n=this.$store.getters;if(t){var r=rl(this.$store,"mapState",t);if(!r)return;e=r.context.state,n=r.context.getters}return"function"==typeof i?i.call(this,e,n):e[i]},n[r].vuex=!0})),n})),Xa=nl((function(t,e){var n={};return el(e).forEach((function(e){var r=e.key,i=e.val;n[r]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var r=this.$store.commit;if(t){var o=rl(this.$store,"mapMutations",t);if(!o)return;r=o.context.commit}return"function"==typeof i?i.apply(this,[r].concat(e)):r.apply(this.$store,[i].concat(e))}})),n})),Qa=nl((function(t,e){var n={};return el(e).forEach((function(e){var r=e.key,i=e.val;i=t+i,n[r]=function(){if(!t||rl(this.$store,"mapGetters",t))return this.$store.getters[i]},n[r].vuex=!0})),n})),tl=nl((function(t,e){var n={};return el(e).forEach((function(e){var r=e.key,i=e.val;n[r]=function(){for(var e=[],n=arguments.length;n--;)e[n]=arguments[n];var r=this.$store.dispatch;if(t){var o=rl(this.$store,"mapActions",t);if(!o)return;r=o.context.dispatch}return"function"==typeof i?i.apply(this,[r].concat(e)):r.apply(this.$store,[i].concat(e))}})),n}));function el(t){return function(t){return Array.isArray(t)||Ra(t)}(t)?Array.isArray(t)?t.map((function(t){return{key:t,val:t}})):Object.keys(t).map((function(e){return{key:e,val:t[e]}})):[]}function nl(t){return function(e,n){return"string"!=typeof e?(n=e,e=""):"/"!==e.charAt(e.length-1)&&(e+="/"),t(e,n)}}function rl(t,e,n){return t._modulesNamespaceMap[n]}function il(t,e,n){var r=n?t.groupCollapsed:t.group;try{r.call(t,e)}catch(_g){t.log(e)}}function ol(t){try{t.groupEnd()}catch(_g){t.log("—— log end ——")}}function sl(){var t=new Date;return" @ "+al(t.getHours(),2)+":"+al(t.getMinutes(),2)+":"+al(t.getSeconds(),2)+"."+al(t.getMilliseconds(),3)}function al(t,e){return n="0",r=e-t.toString().length,new Array(r+1).join(n)+t;var n,r}const ll={Store:Va,install:Ga,version:"3.6.2",mapState:Za,mapMutations:Xa,mapGetters:Qa,mapActions:tl,createNamespacedHelpers:function(t){return{mapState:Za.bind(null,t),mapGetters:Qa.bind(null,t),mapMutations:Xa.bind(null,t),mapActions:tl.bind(null,t)}},createLogger:function(t){void 0===t&&(t={});var e=t.collapsed;void 0===e&&(e=!0);var n=t.filter;void 0===n&&(n=function(t,e,n){return!0});var r=t.transformer;void 0===r&&(r=function(t){return t});var i=t.mutationTransformer;void 0===i&&(i=function(t){return t});var o=t.actionFilter;void 0===o&&(o=function(t,e){return!0});var s=t.actionTransformer;void 0===s&&(s=function(t){return t});var a=t.logMutations;void 0===a&&(a=!0);var l=t.logActions;void 0===l&&(l=!0);var c=t.logger;return void 0===c&&(c=console),function(t){var u=Pa(t.state);void 0!==c&&(a&&t.subscribe((function(t,o){var s=Pa(o);if(n(t,u,s)){var a=sl(),l=i(t),d="mutation "+t.type+a;il(c,d,e),c.log("%c prev state","color: #9E9E9E; font-weight: bold",r(u)),c.log("%c mutation","color: #03A9F4; font-weight: bold",l),c.log("%c next state","color: #4CAF50; font-weight: bold",r(s)),ol(c)}u=s})),l&&t.subscribeAction((function(t,n){if(o(t,n)){var r=sl(),i=s(t),a="action "+t.type+r;il(c,a,e),c.log("%c action","color: #03A9F4; font-weight: bold",i),ol(c)}})))}}};function cl(t){return{all:t=t||new Map,on:function(e,n){var r=t.get(e);r?r.push(n):t.set(e,[n])},off:function(e,n){var r=t.get(e);r&&(n?r.splice(r.indexOf(n)>>>0,1):t.set(e,[]))},emit:function(e,n){var r=t.get(e);r&&r.slice().map((function(t){t(n)})),(r=t.get("*"))&&r.slice().map((function(t){t(e,n)}))}}}var ul,dl,fl="function"==typeof Map?new Map:(ul=[],dl=[],{has:function(t){return ul.indexOf(t)>-1},get:function(t){return dl[ul.indexOf(t)]},set:function(t,e){-1===ul.indexOf(t)&&(ul.push(t),dl.push(e))},delete:function(t){var e=ul.indexOf(t);e>-1&&(ul.splice(e,1),dl.splice(e,1))}}),hl=function(t){return new Event(t,{bubbles:!0})};try{new Event("test")}catch(_g){hl=function(t){var e=document.createEvent("Event");return e.initEvent(t,!0,!1),e}}function pl(t){var e=fl.get(t);e&&e.destroy()}function ml(t){var e=fl.get(t);e&&e.update()}var gl=null;"undefined"==typeof window||"function"!=typeof window.getComputedStyle?((gl=function(t){return t}).destroy=function(t){return t},gl.update=function(t){return t}):((gl=function(t,e){return t&&Array.prototype.forEach.call(t.length?t:[t],(function(t){return function(t){if(t&&t.nodeName&&"TEXTAREA"===t.nodeName&&!fl.has(t)){var e,n=null,r=null,i=null,o=function(){t.clientWidth!==r&&c()},s=function(e){window.removeEventListener("resize",o,!1),t.removeEventListener("input",c,!1),t.removeEventListener("keyup",c,!1),t.removeEventListener("autosize:destroy",s,!1),t.removeEventListener("autosize:update",c,!1),Object.keys(e).forEach((function(n){t.style[n]=e[n]})),fl.delete(t)}.bind(t,{height:t.style.height,resize:t.style.resize,overflowY:t.style.overflowY,overflowX:t.style.overflowX,wordWrap:t.style.wordWrap});t.addEventListener("autosize:destroy",s,!1),"onpropertychange"in t&&"oninput"in t&&t.addEventListener("keyup",c,!1),window.addEventListener("resize",o,!1),t.addEventListener("input",c,!1),t.addEventListener("autosize:update",c,!1),t.style.overflowX="hidden",t.style.wordWrap="break-word",fl.set(t,{destroy:s,update:c}),"vertical"===(e=window.getComputedStyle(t,null)).resize?t.style.resize="none":"both"===e.resize&&(t.style.resize="horizontal"),n="content-box"===e.boxSizing?-(parseFloat(e.paddingTop)+parseFloat(e.paddingBottom)):parseFloat(e.borderTopWidth)+parseFloat(e.borderBottomWidth),isNaN(n)&&(n=0),c()}function a(e){var n=t.style.width;t.style.width="0px",t.style.width=n,t.style.overflowY=e}function l(){if(0!==t.scrollHeight){var e=function(t){for(var e=[];t&&t.parentNode&&t.parentNode instanceof Element;)t.parentNode.scrollTop&&e.push({node:t.parentNode,scrollTop:t.parentNode.scrollTop}),t=t.parentNode;return e}(t),i=document.documentElement&&document.documentElement.scrollTop;t.style.height="",t.style.height=t.scrollHeight+n+"px",r=t.clientWidth,e.forEach((function(t){t.node.scrollTop=t.scrollTop})),i&&(document.documentElement.scrollTop=i)}}function c(){l();var e=Math.round(parseFloat(t.style.height)),n=window.getComputedStyle(t,null),r="content-box"===n.boxSizing?Math.round(parseFloat(n.height)):t.offsetHeight;if(r=e?t:""+Array(e+1-r.length).join(n)+t},y={s:v,z:function(t){var e=-t.utcOffset(),n=Math.abs(e),r=Math.floor(n/60),i=n%60;return(e<=0?"+":"-")+v(r,2,"0")+":"+v(i,2,"0")},m:function t(e,n){if(e.date()1)return t(s[0])}else{var a=e.name;w[a]=e,i=a}return!r&&i&&(b=i),i||!r&&b},k=function(t,e){if(x(t))return t.clone();var n="object"==typeof e?e:{};return n.date=t,n.args=arguments,new _(n)},O=y;O.l=S,O.i=x,O.w=function(t,e){return k(t,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var _=function(){function g(t){this.$L=S(t.locale,null,!0),this.parse(t)}var v=g.prototype;return v.parse=function(t){this.$d=function(t){var e=t.date,n=t.utc;if(null===e)return new Date(NaN);if(O.u(e))return new Date;if(e instanceof Date)return new Date(e);if("string"==typeof e&&!/Z$/i.test(e)){var r=e.match(p);if(r){var i=r[2]-1||0,o=(r[7]||"0").substring(0,3);return n?new Date(Date.UTC(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,o)):new Date(r[1],i,r[3]||1,r[4]||0,r[5]||0,r[6]||0,o)}}return new Date(e)}(t),this.$x=t.x||{},this.init()},v.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},v.$utils=function(){return O},v.isValid=function(){return!(this.$d.toString()===h)},v.isSame=function(t,e){var n=k(t);return this.startOf(e)<=n&&n<=this.endOf(e)},v.isAfter=function(t,e){return k(t)68?1900:2e3)},a=function(t){return function(e){this[t]=+e}},l=[/[+-]\d\d:?(\d\d)?|Z/,function(t){(this.zone||(this.zone={})).offset=function(t){if(!t)return 0;if("Z"===t)return 0;var e=t.match(/([+-]|\d\d)/g),n=60*e[1]+(+e[2]||0);return 0===n?0:"+"===e[0]?-n:n}(t)}],c=function(t){var e=o[t];return e&&(e.indexOf?e:e.s.concat(e.f))},u=function(t,e){var n,r=o.meridiem;if(r){for(var i=1;i<=24;i+=1)if(t.indexOf(r(i,0,e))>-1){n=i>12;break}}else n=t===(e?"pm":"PM");return n},d={A:[i,function(t){this.afternoon=u(t,!1)}],a:[i,function(t){this.afternoon=u(t,!0)}],S:[/\d/,function(t){this.milliseconds=100*+t}],SS:[n,function(t){this.milliseconds=10*+t}],SSS:[/\d{3}/,function(t){this.milliseconds=+t}],s:[r,a("seconds")],ss:[r,a("seconds")],m:[r,a("minutes")],mm:[r,a("minutes")],H:[r,a("hours")],h:[r,a("hours")],HH:[r,a("hours")],hh:[r,a("hours")],D:[r,a("day")],DD:[n,a("day")],Do:[i,function(t){var e=o.ordinal,n=t.match(/\d+/);if(this.day=n[0],e)for(var r=1;r<=31;r+=1)e(r).replace(/\[|\]/g,"")===t&&(this.day=r)}],M:[r,a("month")],MM:[n,a("month")],MMM:[i,function(t){var e=c("months"),n=(c("monthsShort")||e.map((function(t){return t.slice(0,3)}))).indexOf(t)+1;if(n<1)throw new Error;this.month=n%12||n}],MMMM:[i,function(t){var e=c("months").indexOf(t)+1;if(e<1)throw new Error;this.month=e%12||e}],Y:[/[+-]?\d+/,a("year")],YY:[n,function(t){this.year=s(t)}],YYYY:[/\d{4}/,a("year")],Z:l,ZZ:l};function f(n){var r,i;r=n,i=o&&o.formats;for(var s=(n=r.replace(/(\[[^\]]+])|(LTS?|l{1,4}|L{1,4})/g,(function(e,n,r){var o=r&&r.toUpperCase();return n||i[r]||t[r]||i[o].replace(/(\[[^\]]+])|(MMMM|MM|DD|dddd)/g,(function(t,e,n){return e||n.slice(1)}))}))).match(e),a=s.length,l=0;l-1)return new Date(("X"===e?1e3:1)*t);var r=f(e)(t),i=r.year,o=r.month,s=r.day,a=r.hours,l=r.minutes,c=r.seconds,u=r.milliseconds,d=r.zone,h=new Date,p=s||(i||o?1:h.getDate()),m=i||h.getFullYear(),g=0;i&&!o||(g=o>0?o-1:h.getMonth());var v=a||0,y=l||0,b=c||0,w=u||0;return d?new Date(Date.UTC(m,g,p,v,y,b,w+60*d.offset*1e3)):n?new Date(Date.UTC(m,g,p,v,y,b,w)):new Date(m,g,p,v,y,b,w)}catch(x){return new Date("")}}(e,a,r),this.init(),d&&!0!==d&&(this.$L=this.locale(d).$L),u&&e!=this.format(a)&&(this.$d=new Date("")),o={}}else if(a instanceof Array)for(var h=a.length,p=1;p<=h;p+=1){s[1]=a[p-1];var m=n.apply(this,s);if(m.isValid()){this.$d=m.$d,this.$L=m.$L,this.init();break}p===h&&(this.$d=new Date(""))}else i.call(this,t)}}}();function kl(t){return(kl="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var Ol={selector:"vue-portal-target-".concat(((t=21)=>{let e="",n=t;for(;n--;)e+="useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"[64*Math.random()|0];return e})())},_l=function(t){return Ol.selector=t},Ml="undefined"!=typeof window&&void 0!==("undefined"==typeof document?"undefined":kl(document)),Cl=Wn.extend({abstract:!0,name:"PortalOutlet",props:["nodes","tag"],data:function(t){return{updatedNodes:t.nodes}},render:function(t){var e=this.updatedNodes&&this.updatedNodes();return e?1!==e.length||e[0].text?t(this.tag||"DIV",e):e:t()},destroyed:function(){var t=this.$el;t&&t.parentNode.removeChild(t)}}),$l=Wn.extend({name:"VueSimplePortal",props:{disabled:{type:Boolean},prepend:{type:Boolean},selector:{type:String,default:function(){return"#".concat(Ol.selector)}},tag:{type:String,default:"DIV"}},render:function(t){if(this.disabled){var e=this.$scopedSlots&&this.$scopedSlots.default();return e?e.length<2&&!e[0].text?e:t(this.tag,e):t()}return t()},created:function(){this.getTargetEl()||this.insertTargetEl()},updated:function(){var t=this;this.$nextTick((function(){t.disabled||t.slotFn===t.$scopedSlots.default||(t.container.updatedNodes=t.$scopedSlots.default),t.slotFn=t.$scopedSlots.default}))},beforeDestroy:function(){this.unmount()},watch:{disabled:{immediate:!0,handler:function(t){t?this.unmount():this.$nextTick(this.mount)}}},methods:{getTargetEl:function(){if(Ml)return document.querySelector(this.selector)},insertTargetEl:function(){if(Ml){var t=document.querySelector("body"),e=document.createElement(this.tag);e.id=this.selector.substring(1),t.appendChild(e)}},mount:function(){if(Ml){var t=this.getTargetEl(),e=document.createElement("DIV");this.prepend&&t.firstChild?t.insertBefore(e,t.firstChild):t.appendChild(e),this.container=new Cl({el:e,parent:this,propsData:{tag:this.tag,nodes:this.$scopedSlots.default}})}},unmount:function(){this.container&&(this.container.$destroy(),delete this.container)}}});function Tl(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t.component(e.name||"portal",$l),e.defaultSelector&&_l(e.defaultSelector)}"undefined"!=typeof window&&window.Vue&&window.Vue===Wn&&Wn.use(Tl);var Dl={},Nl={};function Al(t){return null==t}function El(t){return null!=t}function Pl(t,e){return e.tag===t.tag&&e.key===t.key}function Il(t){var e=t.tag;t.vm=new e({data:t.args})}function Rl(t,e,n){var r,i,o={};for(r=e;r<=n;++r)El(i=t[r].key)&&(o[i]=r);return o}function zl(t,e,n){for(;e<=n;++e)Il(t[e])}function jl(t,e,n){for(;e<=n;++e){var r=t[e];El(r)&&(r.vm.$destroy(),r.vm=null)}}function Fl(t,e){t!==e&&(e.vm=t.vm,function(t){for(var e=Object.keys(t.args),n=0;na?zl(e,s,u):s>u&&jl(t,o,a)}(t,e):El(e)?zl(e,0,e.length-1):El(t)&&jl(t,0,t.length-1)};var Ll={};function Bl(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);e&&(r=r.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,r)}return n}function Vl(t){for(var e=1;et.length)&&(e=t.length);for(var n=0,r=new Array(e);n1?a:a.$sub[0]:null}}},computed:{run:function(){var t=this,e=this.lazyParentModel();if(Array.isArray(e)&&e.__ob__){var n=e.__ob__.dep;n.depend();var r=n.constructor.target;if(!this._indirectWatcher){var i=r.constructor;this._indirectWatcher=new i(this,(function(){return t.runRule(e)}),null,{lazy:!0})}var o=this.getModel();if(!this._indirectWatcher.dirty&&this._lastModel===o)return this._indirectWatcher.depend(),r.value;this._lastModel=o,this._indirectWatcher.evaluate(),this._indirectWatcher.depend()}else this._indirectWatcher&&(this._indirectWatcher.teardown(),this._indirectWatcher=null);return this._indirectWatcher?this._indirectWatcher.value:this.runRule(e)},$params:function(){return this.run.params},proxy:function(){var t=this.run.output;return t.__isVuelidateAsyncVm?!!t.v:!!t},$pending:function(){var t=this.run.output;return!!t.__isVuelidateAsyncVm&&t.p}},destroyed:function(){this._indirectWatcher&&(this._indirectWatcher.teardown(),this._indirectWatcher=null)}}),a=i.extend({data:function(){return{dirty:!1,validations:null,lazyModel:null,model:null,prop:null,lazyParentModel:null,rootModel:null}},methods:s(s({},g),{},{refProxy:function(t){return this.getRef(t).proxy},getRef:function(t){return this.refs[t]},isNested:function(t){return"function"!=typeof this.validations[t]}}),computed:s(s({},p),{},{nestedKeys:function(){return this.keys.filter(this.isNested)},ruleKeys:function(){var t=this;return this.keys.filter((function(e){return!t.isNested(e)}))},keys:function(){return Object.keys(this.validations).filter((function(t){return"$params"!==t}))},proxy:function(){var t=this,e=u(this.keys,(function(e){return{enumerable:!0,configurable:!0,get:function(){return t.refProxy(e)}}})),n=u(v,(function(e){return{enumerable:!0,configurable:!0,get:function(){return t[e]}}})),r=u(y,(function(e){return{enumerable:!1,configurable:!0,get:function(){return t[e]}}})),i=this.hasIter()?{$iter:{enumerable:!0,value:Object.defineProperties({},s({},e))}}:{};return Object.defineProperties({},s(s(s(s({},e),i),{},{$model:{enumerable:!0,get:function(){var e=t.lazyParentModel();return null!=e?e[t.prop]:null},set:function(e){var n=t.lazyParentModel();null!=n&&(n[t.prop]=e,t.$touch())}}},n),r))},children:function(){var t=this;return[].concat(r(this.nestedKeys.map((function(e){return w(t,e)}))),r(this.ruleKeys.map((function(e){return x(t,e)})))).filter(Boolean)}})}),l=a.extend({methods:{isNested:function(t){return void 0!==this.validations[t]()},getRef:function(t){var e=this;return{get proxy(){return e.validations[t]()||!1}}}}}),m=a.extend({computed:{keys:function(){var t=this.getModel();return f(t)?Object.keys(t):[]},tracker:function(){var t=this,e=this.validations.$trackBy;return e?function(n){return"".concat(h(t.rootModel,t.getModelKey(n),e))}:function(t){return"".concat(t)}},getModelLazy:function(){var t=this;return function(){return t.getModel()}},children:function(){var t=this,n=this.validations,r=this.getModel(),i=s({},n);delete i.$trackBy;var o={};return this.keys.map((function(n){var s=t.tracker(n);return o.hasOwnProperty(s)?null:(o[s]=!0,(0,e.h)(a,s,{validations:i,prop:n,lazyParentModel:t.getModelLazy,model:r[n],rootModel:t.rootModel}))})).filter(Boolean)}},methods:{isNested:function(){return!0},getRef:function(t){return this.refs[this.tracker(t)]},hasIter:function(){return!0}}}),w=function(t,n){if("$each"===n)return(0,e.h)(m,n,{validations:t.validations[n],lazyParentModel:t.lazyParentModel,prop:n,lazyModel:t.getModel,rootModel:t.rootModel});var r=t.validations[n];if(Array.isArray(r)){var i=t.rootModel,o=u(r,(function(t){return function(){return h(i,i.$v,t)}}),(function(t){return Array.isArray(t)?t.join("."):t}));return(0,e.h)(l,n,{validations:o,lazyParentModel:c,prop:n,lazyModel:c,rootModel:i})}return(0,e.h)(a,n,{validations:r,lazyParentModel:t.getModel,prop:n,lazyModel:t.getModelKey,rootModel:t.rootModel})},x=function(t,n){return(0,e.h)(o,n,{rule:t.validations[n],lazyParentModel:t.lazyParentModel,lazyModel:t.getModel,rootModel:t.rootModel})};return b={VBase:i,Validation:a}},x=null;var S=function(t,n){var r=function(t){if(x)return x;for(var e=t.constructor;e.super;)e=e.super;return x=e,e}(t),i=w(r),o=i.Validation;return new(0,i.VBase)({computed:{children:function(){var r="function"==typeof n?n.call(t):n;return[(0,e.h)(o,"$v",{validations:r,lazyParentModel:c,prop:"$v",model:t,rootModel:t})]}}})},k={data:function(){var t=this.$options.validations;return t&&(this._vuelidate=S(this,t)),{}},beforeCreate:function(){var t=this.$options;t.validations&&(t.computed||(t.computed={}),t.computed.$v||(t.computed.$v=function(){return this._vuelidate?this._vuelidate.refs.$v.proxy:null}))},beforeDestroy:function(){this._vuelidate&&(this._vuelidate.$destroy(),this._vuelidate=null)}};function O(t){t.mixin(k)}t.validationMixin=k;var _=O;t.default=_}(Dl);const Zl=yl(Dl);function Xl(t){this.content=t}function Ql(t,e,n){for(let r=0;;r++){if(r==t.childCount||r==e.childCount)return t.childCount==e.childCount?null:n;let i=t.child(r),o=e.child(r);if(i!=o){if(!i.sameMarkup(o))return n;if(i.isText&&i.text!=o.text){for(let t=0;i.text[t]==o.text[t];t++)n++;return n}if(i.content.size||o.content.size){let t=Ql(i.content,o.content,n+1);if(null!=t)return t}n+=i.nodeSize}else n+=i.nodeSize}}function tc(t,e,n,r){for(let i=t.childCount,o=e.childCount;;){if(0==i||0==o)return i==o?null:{a:n,b:r};let s=t.child(--i),a=e.child(--o),l=s.nodeSize;if(s!=a){if(!s.sameMarkup(a))return{a:n,b:r};if(s.isText&&s.text!=a.text){let t=0,e=Math.min(s.text.length,a.text.length);for(;t>1}},Xl.from=function(t){if(t instanceof Xl)return t;var e=[];if(t)for(var n in t)e.push(n,t[n]);return new Xl(e)};class ec{constructor(t,e){if(this.content=t,this.size=e||0,null==e)for(let n=0;nt&&!1!==n(a,r+s,i||null,o)&&a.content.size){let i=s+1;a.nodesBetween(Math.max(0,t-i),Math.min(a.content.size,e-i),n,r+i)}s=l}}descendants(t){this.nodesBetween(0,this.size,t)}textBetween(t,e,n,r){let i="",o=!0;return this.nodesBetween(t,e,((s,a)=>{s.isText?(i+=s.text.slice(Math.max(t,a)-a,e-a),o=!n):s.isLeaf?(r?i+="function"==typeof r?r(s):r:s.type.spec.leafText&&(i+=s.type.spec.leafText(s)),o=!n):!o&&s.isBlock&&(i+=n,o=!0)}),0),i}append(t){if(!t.size)return this;if(!this.size)return t;let e=this.lastChild,n=t.firstChild,r=this.content.slice(),i=0;for(e.isText&&e.sameMarkup(n)&&(r[r.length-1]=e.withText(e.text+n.text),i=1);it)for(let i=0,o=0;ot&&((oe)&&(s=s.isText?s.cut(Math.max(0,t-o),Math.min(s.text.length,e-o)):s.cut(Math.max(0,t-o-1),Math.min(s.content.size,e-o-1))),n.push(s),r+=s.nodeSize),o=a}return new ec(n,r)}cutByIndex(t,e){return t==e?ec.empty:0==t&&e==this.content.length?this:new ec(this.content.slice(t,e))}replaceChild(t,e){let n=this.content[t];if(n==e)return this;let r=this.content.slice(),i=this.size+e.nodeSize-n.nodeSize;return r[t]=e,new ec(r,i)}addToStart(t){return new ec([t].concat(this.content),this.size+t.nodeSize)}addToEnd(t){return new ec(this.content.concat(t),this.size+t.nodeSize)}eq(t){if(this.content.length!=t.content.length)return!1;for(let e=0;ethis.size||t<0)throw new RangeError(`Position ${t} outside of fragment (${this})`);for(let n=0,r=0;;n++){let i=r+this.child(n).nodeSize;if(i>=t)return i==t||e>0?rc(n+1,i):rc(n,r);r=i}}toString(){return"<"+this.toStringInner()+">"}toStringInner(){return this.content.join(", ")}toJSON(){return this.content.length?this.content.map((t=>t.toJSON())):null}static fromJSON(t,e){if(!e)return ec.empty;if(!Array.isArray(e))throw new RangeError("Invalid input for Fragment.fromJSON");return new ec(e.map(t.nodeFromJSON))}static fromArray(t){if(!t.length)return ec.empty;let e,n=0;for(let r=0;rthis.type.rank&&(e||(e=t.slice(0,r)),e.push(this),n=!0),e&&e.push(i)}}return e||(e=t.slice()),n||e.push(this),e}removeFromSet(t){for(let e=0;et.type.rank-e.type.rank)),e}}oc.none=[];class sc extends Error{}class ac{constructor(t,e,n){this.content=t,this.openStart=e,this.openEnd=n}get size(){return this.content.size-this.openStart-this.openEnd}insertAt(t,e){let n=cc(this.content,t+this.openStart,e);return n&&new ac(n,this.openStart,this.openEnd)}removeBetween(t,e){return new ac(lc(this.content,t+this.openStart,e+this.openStart),this.openStart,this.openEnd)}eq(t){return this.content.eq(t.content)&&this.openStart==t.openStart&&this.openEnd==t.openEnd}toString(){return this.content+"("+this.openStart+","+this.openEnd+")"}toJSON(){if(!this.content.size)return null;let t={content:this.content.toJSON()};return this.openStart>0&&(t.openStart=this.openStart),this.openEnd>0&&(t.openEnd=this.openEnd),t}static fromJSON(t,e){if(!e)return ac.empty;let n=e.openStart||0,r=e.openEnd||0;if("number"!=typeof n||"number"!=typeof r)throw new RangeError("Invalid input for Slice.fromJSON");return new ac(ec.fromJSON(t,e.content),n,r)}static maxOpen(t,e=!0){let n=0,r=0;for(let i=t.firstChild;i&&!i.isLeaf&&(e||!i.type.spec.isolating);i=i.firstChild)n++;for(let i=t.lastChild;i&&!i.isLeaf&&(e||!i.type.spec.isolating);i=i.lastChild)r++;return new ac(t,n,r)}}function lc(t,e,n){let{index:r,offset:i}=t.findIndex(e),o=t.maybeChild(r),{index:s,offset:a}=t.findIndex(n);if(i==e||o.isText){if(a!=n&&!t.child(s).isText)throw new RangeError("Removing non-flat range");return t.cut(0,e).append(t.cut(n))}if(r!=s)throw new RangeError("Removing non-flat range");return t.replaceChild(r,o.copy(lc(o.content,e-i-1,n-i-1)))}function cc(t,e,n,r){let{index:i,offset:o}=t.findIndex(e),s=t.maybeChild(i);if(o==e||s.isText)return r&&!r.canReplace(i,i,n)?null:t.cut(0,e).append(n).append(t.cut(e));let a=cc(s.content,e-o-1,n);return a&&t.replaceChild(i,s.copy(a))}function uc(t,e,n){if(n.openStart>t.depth)throw new sc("Inserted content deeper than insertion position");if(t.depth-n.openStart!=e.depth-n.openEnd)throw new sc("Inconsistent open depths");return dc(t,e,n,0)}function dc(t,e,n,r){let i=t.index(r),o=t.node(r);if(i==e.index(r)&&r=0;i--)r=e.node(i).copy(ec.from(r));return{start:r.resolveNoCache(t.openStart+n),end:r.resolveNoCache(r.content.size-t.openEnd-n)}}(n,t);return gc(o,vc(t,i,s,e,r))}{let r=t.parent,i=r.content;return gc(r,i.cut(0,t.parentOffset).append(n.content).append(i.cut(e.parentOffset)))}}return gc(o,yc(t,e,r))}function fc(t,e){if(!e.type.compatibleContent(t.type))throw new sc("Cannot join "+e.type.name+" onto "+t.type.name)}function hc(t,e,n){let r=t.node(n);return fc(r,e.node(n)),r}function pc(t,e){let n=e.length-1;n>=0&&t.isText&&t.sameMarkup(e[n])?e[n]=t.withText(e[n].text+t.text):e.push(t)}function mc(t,e,n,r){let i=(e||t).node(n),o=0,s=e?e.index(n):i.childCount;t&&(o=t.index(n),t.depth>n?o++:t.textOffset&&(pc(t.nodeAfter,r),o++));for(let a=o;ai&&hc(t,e,i+1),s=r.depth>i&&hc(n,r,i+1),a=[];return mc(null,t,i,a),o&&s&&e.index(i)==n.index(i)?(fc(o,s),pc(gc(o,vc(t,e,n,r,i+1)),a)):(o&&pc(gc(o,yc(t,e,i+1)),a),mc(e,n,i,a),s&&pc(gc(s,yc(n,r,i+1)),a)),mc(r,null,i,a),new ec(a)}function yc(t,e,n){let r=[];if(mc(null,t,n,r),t.depth>n){pc(gc(hc(t,e,n+1),yc(t,e,n+1)),r)}return mc(e,null,n,r),new ec(r)}ac.empty=new ac(ec.empty,0,0);class bc{constructor(t,e,n){this.pos=t,this.path=e,this.parentOffset=n,this.depth=e.length/3-1}resolveDepth(t){return null==t?this.depth:t<0?this.depth+t:t}get parent(){return this.node(this.depth)}get doc(){return this.node(0)}node(t){return this.path[3*this.resolveDepth(t)]}index(t){return this.path[3*this.resolveDepth(t)+1]}indexAfter(t){return t=this.resolveDepth(t),this.index(t)+(t!=this.depth||this.textOffset?1:0)}start(t){return 0==(t=this.resolveDepth(t))?0:this.path[3*t-1]+1}end(t){return t=this.resolveDepth(t),this.start(t)+this.node(t).content.size}before(t){if(!(t=this.resolveDepth(t)))throw new RangeError("There is no position before the top-level node");return t==this.depth+1?this.pos:this.path[3*t-1]}after(t){if(!(t=this.resolveDepth(t)))throw new RangeError("There is no position after the top-level node");return t==this.depth+1?this.pos:this.path[3*t-1]+this.path[3*t].nodeSize}get textOffset(){return this.pos-this.path[this.path.length-1]}get nodeAfter(){let t=this.parent,e=this.index(this.depth);if(e==t.childCount)return null;let n=this.pos-this.path[this.path.length-1],r=t.child(e);return n?t.child(e).cut(n):r}get nodeBefore(){let t=this.index(this.depth),e=this.pos-this.path[this.path.length-1];return e?this.parent.child(t).cut(0,e):0==t?null:this.parent.child(t-1)}posAtIndex(t,e){e=this.resolveDepth(e);let n=this.path[3*e],r=0==e?0:this.path[3*e-1]+1;for(let i=0;i0;e--)if(this.start(e)<=t&&this.end(e)>=t)return e;return 0}blockRange(t=this,e){if(t.pos=0;n--)if(t.pos<=this.end(n)&&(!e||e(this.node(n))))return new kc(this,t,n);return null}sameParent(t){return this.pos-this.parentOffset==t.pos-t.parentOffset}max(t){return t.pos>this.pos?t:this}min(t){return t.pos=0&&e<=t.content.size))throw new RangeError("Position "+e+" out of range");let n=[],r=0,i=e;for(let o=t;;){let{index:t,offset:e}=o.content.findIndex(i),s=i-e;if(n.push(o,t,r+e),!s)break;if(o=o.child(t),o.isText)break;i=s-1,r+=e+1}return new bc(e,n,i)}static resolveCached(t,e){for(let r=0;rt&&this.nodesBetween(t,e,(t=>(n.isInSet(t.marks)&&(r=!0),!r))),r}get isBlock(){return this.type.isBlock}get isTextblock(){return this.type.isTextblock}get inlineContent(){return this.type.inlineContent}get isInline(){return this.type.isInline}get isText(){return this.type.isText}get isLeaf(){return this.type.isLeaf}get isAtom(){return this.type.isAtom}toString(){if(this.type.spec.toDebugString)return this.type.spec.toDebugString(this);let t=this.type.name;return this.content.size&&(t+="("+this.content.toStringInner()+")"),Cc(this.marks,t)}contentMatchAt(t){let e=this.type.contentMatch.matchFragment(this.content,0,t);if(!e)throw new Error("Called contentMatchAt on a node with invalid content");return e}canReplace(t,e,n=ec.empty,r=0,i=n.childCount){let o=this.contentMatchAt(t).matchFragment(n,r,i),s=o&&o.matchFragment(this.content,e);if(!s||!s.validEnd)return!1;for(let a=r;at.type.name))}`);this.content.forEach((t=>t.check()))}toJSON(){let t={type:this.type.name};for(let e in this.attrs){t.attrs=this.attrs;break}return this.content.size&&(t.content=this.content.toJSON()),this.marks.length&&(t.marks=this.marks.map((t=>t.toJSON()))),t}static fromJSON(t,e){if(!e)throw new RangeError("Invalid input for Node.fromJSON");let n=null;if(e.marks){if(!Array.isArray(e.marks))throw new RangeError("Invalid mark data for Node.fromJSON");n=e.marks.map(t.markFromJSON)}if("text"==e.type){if("string"!=typeof e.text)throw new RangeError("Invalid text node in JSON");return t.text(e.text,n)}let r=ec.fromJSON(t,e.content);return t.nodeType(e.type).create(e.attrs,r,n)}}_c.prototype.text=void 0;class Mc extends _c{constructor(t,e,n,r){if(super(t,e,null,r),!n)throw new RangeError("Empty text nodes are not allowed");this.text=n}toString(){return this.type.spec.toDebugString?this.type.spec.toDebugString(this):Cc(this.marks,JSON.stringify(this.text))}get textContent(){return this.text}textBetween(t,e){return this.text.slice(t,e)}get nodeSize(){return this.text.length}mark(t){return t==this.marks?this:new Mc(this.type,this.attrs,this.text,t)}withText(t){return t==this.text?this:new Mc(this.type,this.attrs,t,this.marks)}cut(t=0,e=this.text.length){return 0==t&&e==this.text.length?this:this.withText(this.text.slice(t,e))}eq(t){return this.sameMarkup(t)&&this.text==t.text}toJSON(){let t=super.toJSON();return t.text=this.text,t}}function Cc(t,e){for(let n=t.length-1;n>=0;n--)e=t[n].type.name+"("+e+")";return e}class $c{constructor(t){this.validEnd=t,this.next=[],this.wrapCache=[]}static parse(t,e){let n=new Tc(t,e);if(null==n.next)return $c.empty;let r=Dc(n);n.next&&n.err("Unexpected trailing text");let i=function(t){let e=Object.create(null);return n(Rc(t,0));function n(r){let i=[];r.forEach((e=>{t[e].forEach((({term:e,to:n})=>{if(!e)return;let r;for(let t=0;t{r||i.push([e,r=[]]),-1==r.indexOf(t)&&r.push(t)}))}))}));let o=e[r.join(",")]=new $c(r.indexOf(t.length-1)>-1);for(let t=0;tt.to=e))}function o(t,e){if("choice"==t.type)return t.exprs.reduce(((t,n)=>t.concat(o(n,e))),[]);if("seq"!=t.type){if("star"==t.type){let s=n();return r(e,s),i(o(t.expr,s),s),[r(s)]}if("plus"==t.type){let s=n();return i(o(t.expr,e),s),i(o(t.expr,s),s),[r(s)]}if("opt"==t.type)return[r(e)].concat(o(t.expr,e));if("range"==t.type){let s=e;for(let e=0;et.createAndFill())));for(let t=0;t=this.next.length)throw new RangeError(`There's no ${t}th edge in this content match`);return this.next[t]}toString(){let t=[];return function e(n){t.push(n);for(let r=0;r{let r=n+(e.validEnd?"*":" ")+" ";for(let i=0;i"+t.indexOf(e.next[i].next);return r})).join("\n")}}$c.empty=new $c(!0);class Tc{constructor(t,e){this.string=t,this.nodeTypes=e,this.inline=null,this.pos=0,this.tokens=t.split(/\s*(?=\b|\W|$)/),""==this.tokens[this.tokens.length-1]&&this.tokens.pop(),""==this.tokens[0]&&this.tokens.shift()}get next(){return this.tokens[this.pos]}eat(t){return this.next==t&&(this.pos++||!0)}err(t){throw new SyntaxError(t+" (in content expression '"+this.string+"')")}}function Dc(t){let e=[];do{e.push(Nc(t))}while(t.eat("|"));return 1==e.length?e[0]:{type:"choice",exprs:e}}function Nc(t){let e=[];do{e.push(Ac(t))}while(t.next&&")"!=t.next&&"|"!=t.next);return 1==e.length?e[0]:{type:"seq",exprs:e}}function Ac(t){let e=function(t){if(t.eat("(")){let e=Dc(t);return t.eat(")")||t.err("Missing closing paren"),e}if(!/\W/.test(t.next)){let e=function(t,e){let n=t.nodeTypes,r=n[e];if(r)return[r];let i=[];for(let o in n){let t=n[o];t.groups.indexOf(e)>-1&&i.push(t)}0==i.length&&t.err("No node type or group '"+e+"' found");return i}(t,t.next).map((e=>(null==t.inline?t.inline=e.isInline:t.inline!=e.isInline&&t.err("Mixing inline and block content"),{type:"name",value:e})));return t.pos++,1==e.length?e[0]:{type:"choice",exprs:e}}t.err("Unexpected token '"+t.next+"'")}(t);for(;;)if(t.eat("+"))e={type:"plus",expr:e};else if(t.eat("*"))e={type:"star",expr:e};else if(t.eat("?"))e={type:"opt",expr:e};else{if(!t.eat("{"))break;e=Pc(t,e)}return e}function Ec(t){/\D/.test(t.next)&&t.err("Expected number, got '"+t.next+"'");let e=Number(t.next);return t.pos++,e}function Pc(t,e){let n=Ec(t),r=n;return t.eat(",")&&(r="}"!=t.next?Ec(t):-1),t.eat("}")||t.err("Unclosed braced range"),{type:"range",min:n,max:r,expr:e}}function Ic(t,e){return e-t}function Rc(t,e){let n=[];return function e(r){let i=t[r];if(1==i.length&&!i[0].term)return e(i[0].to);n.push(r);for(let t=0;t-1}allowsMarks(t){if(null==this.markSet)return!0;for(let e=0;en[t]=new Lc(t,e,r)));let r=e.spec.topNode||"doc";if(!n[r])throw new RangeError("Schema is missing its top node type ('"+r+"')");if(!n.text)throw new RangeError("Every schema needs a 'text' type");for(let i in n.text.attrs)throw new RangeError("The text node type should not have attributes");return n}}class Bc{constructor(t){this.hasDefault=Object.prototype.hasOwnProperty.call(t,"default"),this.default=t.default}get isRequired(){return!this.hasDefault}}class Vc{constructor(t,e,n,r){this.name=t,this.rank=e,this.schema=n,this.spec=r,this.attrs=Fc(r.attrs),this.excluded=null;let i=zc(this.attrs);this.instance=i?new oc(this,i):null}create(t=null){return!t&&this.instance?this.instance:new oc(this,jc(this.attrs,t))}static compile(t,e){let n=Object.create(null),r=0;return t.forEach(((t,i)=>n[t]=new Vc(t,r++,e,i))),n}removeFromSet(t){for(var e=0;e-1}}class Wc{constructor(t){this.cached=Object.create(null),this.spec={nodes:Xl.from(t.nodes),marks:Xl.from(t.marks||{}),topNode:t.topNode},this.nodes=Lc.compile(this.spec.nodes,this),this.marks=Vc.compile(this.spec.marks,this);let e=Object.create(null);for(let n in this.nodes){if(n in this.marks)throw new RangeError(n+" can not be both a node and a mark");let t=this.nodes[n],r=t.spec.content||"",i=t.spec.marks;t.contentMatch=e[r]||(e[r]=$c.parse(r,this.nodes)),t.inlineContent=t.contentMatch.inlineContent,t.markSet="_"==i?null:i?qc(this,i.split(" ")):""!=i&&t.inlineContent?null:[]}for(let n in this.marks){let t=this.marks[n],e=t.spec.excludes;t.excluded=null==e?[t]:""==e?[]:qc(this,e.split(" "))}this.nodeFromJSON=this.nodeFromJSON.bind(this),this.markFromJSON=this.markFromJSON.bind(this),this.topNodeType=this.nodes[this.spec.topNode||"doc"],this.cached.wrappings=Object.create(null)}node(t,e=null,n,r){if("string"==typeof t)t=this.nodeType(t);else{if(!(t instanceof Lc))throw new RangeError("Invalid node type: "+t);if(t.schema!=this)throw new RangeError("Node type from different schema used ("+t.name+")")}return t.createChecked(e,n,r)}text(t,e){let n=this.nodes.text;return new Mc(n,n.defaultAttrs,t,oc.setFrom(e))}mark(t,e){return"string"==typeof t&&(t=this.marks[t]),t.create(e)}nodeFromJSON(t){return _c.fromJSON(this,t)}markFromJSON(t){return oc.fromJSON(this,t)}nodeType(t){let e=this.nodes[t];if(!e)throw new RangeError("Unknown node type: "+t);return e}}function qc(t,e){let n=[];for(let r=0;r-1)&&n.push(s=r)}if(!s)throw new SyntaxError("Unknown mark type: '"+e[r]+"'")}return n}class Jc{constructor(t,e){this.schema=t,this.rules=e,this.tags=[],this.styles=[],e.forEach((t=>{t.tag?this.tags.push(t):t.style&&this.styles.push(t)})),this.normalizeLists=!this.tags.some((e=>{if(!/^(ul|ol)\b/.test(e.tag)||!e.node)return!1;let n=t.nodes[e.node];return n.contentMatch.matchType(n)}))}parse(t,e={}){let n=new Zc(this,e,!1);return n.addAll(t,e.from,e.to),n.finish()}parseSlice(t,e={}){let n=new Zc(this,e,!0);return n.addAll(t,e.from,e.to),ac.maxOpen(n.finish())}matchTag(t,e,n){for(let r=n?this.tags.indexOf(n)+1:0;rt.length&&(61!=o.charCodeAt(t.length)||o.slice(t.length+1)!=e))){if(r.getAttrs){let t=r.getAttrs(e);if(!1===t)continue;r.attrs=t||void 0}return r}}}static schemaRules(t){let e=[];function n(t){let n=null==t.priority?50:t.priority,r=0;for(;r{n(t=Qc(t)),t.mark=r}))}for(let r in t.nodes){let e=t.nodes[r].spec.parseDOM;e&&e.forEach((t=>{n(t=Qc(t)),t.node=r}))}return e}static fromSchema(t){return t.cached.domParser||(t.cached.domParser=new Jc(t,Jc.schemaRules(t)))}}const Kc={address:!0,article:!0,aside:!0,blockquote:!0,canvas:!0,dd:!0,div:!0,dl:!0,fieldset:!0,figcaption:!0,figure:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,li:!0,noscript:!0,ol:!0,output:!0,p:!0,pre:!0,section:!0,table:!0,tfoot:!0,ul:!0},Hc={head:!0,noscript:!0,object:!0,script:!0,style:!0,title:!0},Uc={ol:!0,ul:!0};function Yc(t,e,n){return null!=e?(e?1:0)|("full"===e?2:0):t&&"pre"==t.whitespace?3:-5&n}class Gc{constructor(t,e,n,r,i,o,s){this.type=t,this.attrs=e,this.marks=n,this.pendingMarks=r,this.solid=i,this.options=s,this.content=[],this.activeMarks=oc.none,this.stashMarks=[],this.match=o||(4&s?null:t.contentMatch)}findWrapping(t){if(!this.match){if(!this.type)return[];let e=this.type.contentMatch.fillBefore(ec.from(t));if(!e){let e,n=this.type.contentMatch;return(e=n.findWrapping(t.type))?(this.match=n,e):null}this.match=this.type.contentMatch.matchFragment(e)}return this.match.findWrapping(t.type)}finish(t){if(!(1&this.options)){let t,e=this.content[this.content.length-1];if(e&&e.isText&&(t=/[ \t\r\n\u000c]+$/.exec(e.text))){let n=e;e.text.length==t[0].length?this.content.pop():this.content[this.content.length-1]=n.withText(n.text.slice(0,n.text.length-t[0].length))}}let e=ec.from(this.content);return!t&&this.match&&(e=e.append(this.match.fillBefore(ec.empty,!0))),this.type?this.type.create(this.attrs,e,this.marks):e}popFromStashMark(t){for(let e=this.stashMarks.length-1;e>=0;e--)if(t.eq(this.stashMarks[e]))return this.stashMarks.splice(e,1)[0]}applyPending(t){for(let e=0,n=this.pendingMarks;ethis.insertNode(t)));else{let n=t;"string"==typeof e.contentElement?n=t.querySelector(e.contentElement):"function"==typeof e.contentElement?n=e.contentElement(t):e.contentElement&&(n=e.contentElement),this.findAround(t,n,!0),this.addAll(n)}r&&this.sync(s)&&this.open--,o&&this.removePendingMark(o,s)}addAll(t,e,n){let r=e||0;for(let i=e?t.childNodes[e]:t.firstChild,o=null==n?null:t.childNodes[n];i!=o;i=i.nextSibling,++r)this.findAtPoint(t,r),this.addDOM(i);this.findAtPoint(t,r)}findPlace(t){let e,n;for(let r=this.open;r>=0;r--){let i=this.nodes[r],o=i.findWrapping(t);if(o&&(!e||e.length>o.length)&&(e=o,n=i,!o.length))break;if(i.solid)break}if(!e)return!1;this.sync(n);for(let r=0;rthis.open){for(;e>this.open;e--)this.nodes[e-1].content.push(this.nodes[e].finish(t));this.nodes.length=this.open+1}}finish(){return this.open=0,this.closeExtra(this.isOpen),this.nodes[0].finish(this.isOpen||this.options.topOpen)}sync(t){for(let e=this.open;e>=0;e--)if(this.nodes[e]==t)return this.open=e,!0;return!1}get currentPos(){this.closeExtra();let t=0;for(let e=this.open;e>=0;e--){let n=this.nodes[e].content;for(let e=n.length-1;e>=0;e--)t+=n[e].nodeSize;e&&t++}return t}findAtPoint(t,e){if(this.find)for(let n=0;n-1)return t.split(/\s*\|\s*/).some(this.matchesContext,this);let e=t.split("/"),n=this.options.context,r=!(this.isOpen||n&&n.parent.type!=this.nodes[0].type),i=-(n?n.depth+1:0)+(r?0:1),o=(t,s)=>{for(;t>=0;t--){let a=e[t];if(""==a){if(t==e.length-1||0==t)continue;for(;s>=i;s--)if(o(t-1,s))return!0;return!1}{let t=s>0||0==s&&r?this.nodes[s].type:n&&s>=i?n.node(s-i).type:null;if(!t||t.name!=a&&-1==t.groups.indexOf(a))return!1;s--}}return!0};return o(e.length-1,this.open)}textblockFromContext(){let t=this.options.context;if(t)for(let e=t.depth;e>=0;e--){let n=t.node(e).contentMatchAt(t.indexAfter(e)).defaultType;if(n&&n.isTextblock&&n.defaultAttrs)return n}for(let e in this.parser.schema.nodes){let t=this.parser.schema.nodes[e];if(t.isTextblock&&t.defaultAttrs)return t}}addPendingMark(t){let e=function(t,e){for(let n=0;n=0;n--){let r=this.nodes[n];if(r.pendingMarks.lastIndexOf(t)>-1)r.pendingMarks=t.removeFromSet(r.pendingMarks);else{r.activeMarks=t.removeFromSet(r.activeMarks);let e=r.popFromStashMark(t);e&&r.type&&r.type.allowsMarkType(e.type)&&(r.activeMarks=e.addToSet(r.activeMarks))}if(r==e)break}}}function Xc(t,e){return(t.matches||t.msMatchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector).call(t,e)}function Qc(t){let e={};for(let n in t)e[n]=t[n];return e}function tu(t,e){let n=e.schema.nodes;for(let r in n){let i=n[r];if(!i.allowsMarkType(t))continue;let o=[],s=t=>{o.push(t);for(let n=0;n{if(i.length||t.marks.length){let n=0,o=0;for(;n=0;r--){let i=this.serializeMark(t.marks[r],t.isInline,e);i&&((i.contentDOM||i.dom).appendChild(n),n=i.dom)}return n}serializeMark(t,e,n={}){let r=this.marks[t.type.name];return r&&eu.renderSpec(ru(n),r(t,e))}static renderSpec(t,e,n=null){if("string"==typeof e)return{dom:t.createTextNode(e)};if(null!=e.nodeType)return{dom:e};if(e.dom&&null!=e.dom.nodeType)return e;let r,i=e[0],o=i.indexOf(" ");o>0&&(n=i.slice(0,o),i=i.slice(o+1));let s=n?t.createElementNS(n,i):t.createElement(i),a=e[1],l=1;if(a&&"object"==typeof a&&null==a.nodeType&&!Array.isArray(a)){l=2;for(let t in a)if(null!=a[t]){let e=t.indexOf(" ");e>0?s.setAttributeNS(t.slice(0,e),t.slice(e+1),a[t]):s.setAttribute(t,a[t])}}for(let c=l;cl)throw new RangeError("Content hole must be the only child of its parent node");return{dom:s,contentDOM:s}}{let{dom:e,contentDOM:o}=eu.renderSpec(t,i,n);if(s.appendChild(e),o){if(r)throw new RangeError("Multiple content holes");r=o}}}return{dom:s,contentDOM:r}}static fromSchema(t){return t.cached.domSerializer||(t.cached.domSerializer=new eu(this.nodesFromSchema(t),this.marksFromSchema(t)))}static nodesFromSchema(t){let e=nu(t.nodes);return e.text||(e.text=t=>t.text),e}static marksFromSchema(t){return nu(t.marks)}}function nu(t){let e={};for(let n in t){let r=t[n].spec.toDOM;r&&(e[n]=r)}return e}function ru(t){return t.document||window.document}const iu=Math.pow(2,16);function ou(t){return 65535&t}class su{constructor(t,e=!1,n=null){this.pos=t,this.deleted=e,this.recover=n}}class au{constructor(t,e=!1){if(this.ranges=t,this.inverted=e,!t.length&&au.empty)return au.empty}recover(t){let e=0,n=ou(t);if(!this.inverted)for(let r=0;rt)break;let l=this.ranges[s+i],c=this.ranges[s+o],u=a+l;if(t<=u){let i=a+r+((l?t==a?-1:t==u?1:e:e)<0?0:c);if(n)return i;let o=t==(e<0?a:u)?null:s/3+(t-a)*iu;return new su(i,e<0?t!=a:t!=u,o)}r+=c-l}return n?t+r:new su(t+r)}touches(t,e){let n=0,r=ou(e),i=this.inverted?2:1,o=this.inverted?1:2;for(let s=0;st)break;let a=this.ranges[s+i];if(t<=e+a&&s==3*r)return!0;n+=this.ranges[s+o]-a}return!1}forEach(t){let e=this.inverted?2:1,n=this.inverted?1:2;for(let r=0,i=0;r=0;e--){let r=t.getMirror(e);this.appendMap(t.maps[e].invert(),null!=r&&r>e?n-r-1:void 0)}}invert(){let t=new lu;return t.appendMappingInverted(this),t}map(t,e=1){if(this.mirror)return this._map(t,e,!0);for(let n=this.from;ni&&et.isAtom&&e.type.allowsMarkType(this.mark.type)?t.mark(this.mark.addToSet(t.marks)):t),r),e.openStart,e.openEnd);return du.fromReplace(t,this.from,this.to,i)}invert(){return new pu(this.from,this.to,this.mark)}map(t){let e=t.mapResult(this.from,1),n=t.mapResult(this.to,-1);return e.deleted&&n.deleted||e.pos>=n.pos?null:new hu(e.pos,n.pos,this.mark)}merge(t){return t instanceof hu&&t.mark.eq(this.mark)&&this.from<=t.to&&this.to>=t.from?new hu(Math.min(this.from,t.from),Math.max(this.to,t.to),this.mark):null}toJSON(){return{stepType:"addMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(t,e){if("number"!=typeof e.from||"number"!=typeof e.to)throw new RangeError("Invalid input for AddMarkStep.fromJSON");return new hu(e.from,e.to,t.markFromJSON(e.mark))}}uu.jsonID("addMark",hu);class pu extends uu{constructor(t,e,n){super(),this.from=t,this.to=e,this.mark=n}apply(t){let e=t.slice(this.from,this.to),n=new ac(fu(e.content,(t=>t.mark(this.mark.removeFromSet(t.marks))),t),e.openStart,e.openEnd);return du.fromReplace(t,this.from,this.to,n)}invert(){return new hu(this.from,this.to,this.mark)}map(t){let e=t.mapResult(this.from,1),n=t.mapResult(this.to,-1);return e.deleted&&n.deleted||e.pos>=n.pos?null:new pu(e.pos,n.pos,this.mark)}merge(t){return t instanceof pu&&t.mark.eq(this.mark)&&this.from<=t.to&&this.to>=t.from?new pu(Math.min(this.from,t.from),Math.max(this.to,t.to),this.mark):null}toJSON(){return{stepType:"removeMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(t,e){if("number"!=typeof e.from||"number"!=typeof e.to)throw new RangeError("Invalid input for RemoveMarkStep.fromJSON");return new pu(e.from,e.to,t.markFromJSON(e.mark))}}uu.jsonID("removeMark",pu);class mu extends uu{constructor(t,e,n,r=!1){super(),this.from=t,this.to=e,this.slice=n,this.structure=r}apply(t){return this.structure&&vu(t,this.from,this.to)?du.fail("Structure replace would overwrite content"):du.fromReplace(t,this.from,this.to,this.slice)}getMap(){return new au([this.from,this.to-this.from,this.slice.size])}invert(t){return new mu(this.from,this.from+this.slice.size,t.slice(this.from,this.to))}map(t){let e=t.mapResult(this.from,1),n=t.mapResult(this.to,-1);return e.deleted&&n.deleted?null:new mu(e.pos,Math.max(e.pos,n.pos),this.slice)}merge(t){if(!(t instanceof mu)||t.structure||this.structure)return null;if(this.from+this.slice.size!=t.from||this.slice.openEnd||t.slice.openStart){if(t.to!=this.from||this.slice.openStart||t.slice.openEnd)return null;{let e=this.slice.size+t.slice.size==0?ac.empty:new ac(t.slice.content.append(this.slice.content),t.slice.openStart,this.slice.openEnd);return new mu(t.from,this.to,e,this.structure)}}{let e=this.slice.size+t.slice.size==0?ac.empty:new ac(this.slice.content.append(t.slice.content),this.slice.openStart,t.slice.openEnd);return new mu(this.from,this.to+(t.to-t.from),e,this.structure)}}toJSON(){let t={stepType:"replace",from:this.from,to:this.to};return this.slice.size&&(t.slice=this.slice.toJSON()),this.structure&&(t.structure=!0),t}static fromJSON(t,e){if("number"!=typeof e.from||"number"!=typeof e.to)throw new RangeError("Invalid input for ReplaceStep.fromJSON");return new mu(e.from,e.to,ac.fromJSON(t,e.slice),!!e.structure)}}uu.jsonID("replace",mu);class gu extends uu{constructor(t,e,n,r,i,o,s=!1){super(),this.from=t,this.to=e,this.gapFrom=n,this.gapTo=r,this.slice=i,this.insert=o,this.structure=s}apply(t){if(this.structure&&(vu(t,this.from,this.gapFrom)||vu(t,this.gapTo,this.to)))return du.fail("Structure gap-replace would overwrite content");let e=t.slice(this.gapFrom,this.gapTo);if(e.openStart||e.openEnd)return du.fail("Gap is not a flat range");let n=this.slice.insertAt(this.insert,e.content);return n?du.fromReplace(t,this.from,this.to,n):du.fail("Content does not fit in gap")}getMap(){return new au([this.from,this.gapFrom-this.from,this.insert,this.gapTo,this.to-this.gapTo,this.slice.size-this.insert])}invert(t){let e=this.gapTo-this.gapFrom;return new gu(this.from,this.from+this.slice.size+e,this.from+this.insert,this.from+this.insert+e,t.slice(this.from,this.to).removeBetween(this.gapFrom-this.from,this.gapTo-this.from),this.gapFrom-this.from,this.structure)}map(t){let e=t.mapResult(this.from,1),n=t.mapResult(this.to,-1),r=t.map(this.gapFrom,-1),i=t.map(this.gapTo,1);return e.deleted&&n.deleted||rn.pos?null:new gu(e.pos,n.pos,r,i,this.slice,this.insert,this.structure)}toJSON(){let t={stepType:"replaceAround",from:this.from,to:this.to,gapFrom:this.gapFrom,gapTo:this.gapTo,insert:this.insert};return this.slice.size&&(t.slice=this.slice.toJSON()),this.structure&&(t.structure=!0),t}static fromJSON(t,e){if("number"!=typeof e.from||"number"!=typeof e.to||"number"!=typeof e.gapFrom||"number"!=typeof e.gapTo||"number"!=typeof e.insert)throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON");return new gu(e.from,e.to,e.gapFrom,e.gapTo,ac.fromJSON(t,e.slice),e.insert,!!e.structure)}}function vu(t,e,n){let r=t.resolve(e),i=n-e,o=r.depth;for(;i>0&&o>0&&r.indexAfter(o)==r.node(o).childCount;)o--,i--;if(i>0){let t=r.node(o).maybeChild(r.indexAfter(o));for(;i>0;){if(!t||t.isLeaf)return!0;t=t.firstChild,i--}}return!1}function yu(t,e,n){return(0==e||t.canReplace(e,t.childCount))&&(n==t.childCount||t.canReplace(0,n))}function bu(t){let e=t.parent.content.cutByIndex(t.startIndex,t.endIndex);for(let n=t.depth;;--n){let r=t.$from.node(n),i=t.$from.index(n),o=t.$to.indexAfter(n);if(no;c--,u--){let t=i.node(c),e=i.index(c);if(t.type.spec.isolating)return!1;let n=t.content.cutByIndex(e,t.childCount),o=r&&r[u]||t;if(o!=t&&(n=n.replaceChild(0,o.type.create(o.attrs))),!t.canReplace(e+1,t.childCount)||!o.type.validContent(n))return!1}let a=i.indexAfter(o),l=r&&r[0];return i.node(o).canReplaceWith(a,a,l?l.type:i.node(o+1).type)}function ku(t,e){let n=t.resolve(e),r=n.index();return i=n.nodeBefore,o=n.nodeAfter,!(!i||!o||i.isLeaf||!i.canAppend(o))&&n.parent.canReplace(r,r+1);var i,o}function Ou(t,e,n=e,r=ac.empty){if(e==n&&!r.size)return null;let i=t.resolve(e),o=t.resolve(n);return _u(i,o,r)?new mu(e,n,r):new Mu(i,o,r).fit()}function _u(t,e,n){return!n.openStart&&!n.openEnd&&t.start()==e.start()&&t.parent.canReplace(t.index(),e.index(),n.content)}uu.jsonID("replaceAround",gu);class Mu{constructor(t,e,n){this.$from=t,this.$to=e,this.unplaced=n,this.frontier=[],this.placed=ec.empty;for(let r=0;r<=t.depth;r++){let e=t.node(r);this.frontier.push({type:e.type,match:e.contentMatchAt(t.indexAfter(r))})}for(let r=t.depth;r>0;r--)this.placed=ec.from(t.node(r).copy(this.placed))}get depth(){return this.frontier.length-1}fit(){for(;this.unplaced.size;){let t=this.findFittable();t?this.placeNodes(t):this.openMore()||this.dropNode()}let t=this.mustMoveInline(),e=this.placed.size-this.depth-this.$from.depth,n=this.$from,r=this.close(t<0?this.$to:n.doc.resolve(t));if(!r)return null;let i=this.placed,o=n.depth,s=r.depth;for(;o&&s&&1==i.childCount;)i=i.firstChild.content,o--,s--;let a=new ac(i,o,s);return t>-1?new gu(n.pos,t,this.$to.pos,this.$to.end(),a,e):a.size||n.pos!=this.$to.pos?new mu(n.pos,r.pos,a):null}findFittable(){for(let t=1;t<=2;t++)for(let e=this.unplaced.openStart;e>=0;e--){let n,r=null;e?(r=Tu(this.unplaced.content,e-1).firstChild,n=r.content):n=this.unplaced.content;let i=n.firstChild;for(let o=this.depth;o>=0;o--){let n,{type:s,match:a}=this.frontier[o],l=null;if(1==t&&(i?a.matchType(i.type)||(l=a.fillBefore(ec.from(i),!1)):r&&s.compatibleContent(r.type)))return{sliceDepth:e,frontierDepth:o,parent:r,inject:l};if(2==t&&i&&(n=a.findWrapping(i.type)))return{sliceDepth:e,frontierDepth:o,parent:r,wrap:n};if(r&&a.matchType(r.type))break}}}openMore(){let{content:t,openStart:e,openEnd:n}=this.unplaced,r=Tu(t,e);return!(!r.childCount||r.firstChild.isLeaf)&&(this.unplaced=new ac(t,e+1,Math.max(n,r.size+e>=t.size-n?e+1:0)),!0)}dropNode(){let{content:t,openStart:e,openEnd:n}=this.unplaced,r=Tu(t,e);if(r.childCount<=1&&e>0){let i=t.size-e<=e+r.size;this.unplaced=new ac(Cu(t,e-1,1),e-1,i?e-1:n)}else this.unplaced=new ac(Cu(t,e,1),e,n)}placeNodes({sliceDepth:t,frontierDepth:e,parent:n,inject:r,wrap:i}){for(;this.depth>e;)this.closeFrontierNode();if(i)for(let p=0;p1||0==a||t.content.size)&&(u=e,c.push(Du(t.mark(d.allowedMarks(t.marks)),1==l?a:0,l==s.childCount?f:-1)))}let h=l==s.childCount;h||(f=-1),this.placed=$u(this.placed,e,ec.from(c)),this.frontier[e].match=u,h&&f<0&&n&&n.type==this.frontier[this.depth].type&&this.frontier.length>1&&this.closeFrontierNode();for(let p=0,m=s;p1&&r==this.$to.end(--n);)++r;return r}findCloseLevel(t){t:for(let e=Math.min(this.depth,t.depth);e>=0;e--){let{match:n,type:r}=this.frontier[e],i=e=0;n--){let{match:e,type:r}=this.frontier[n],i=Nu(t,n,r,e,!0);if(!i||i.childCount)continue t}return{depth:e,fit:o,move:i?t.doc.resolve(t.after(e+1)):t}}}}close(t){let e=this.findCloseLevel(t);if(!e)return null;for(;this.depth>e.depth;)this.closeFrontierNode();e.fit.childCount&&(this.placed=$u(this.placed,e.depth,e.fit)),t=e.move;for(let n=e.depth+1;n<=t.depth;n++){let e=t.node(n),r=e.type.contentMatch.fillBefore(e.content,!0,t.index(n));this.openFrontierNode(e.type,e.attrs,r)}return t}openFrontierNode(t,e=null,n){let r=this.frontier[this.depth];r.match=r.match.matchType(t),this.placed=$u(this.placed,this.depth,ec.from(t.create(e,n))),this.frontier.push({type:t,match:t.contentMatch})}closeFrontierNode(){let t=this.frontier.pop().match.fillBefore(ec.empty,!0);t.childCount&&(this.placed=$u(this.placed,this.frontier.length,t))}}function Cu(t,e,n){return 0==e?t.cutByIndex(n,t.childCount):t.replaceChild(0,t.firstChild.copy(Cu(t.firstChild.content,e-1,n)))}function $u(t,e,n){return 0==e?t.append(n):t.replaceChild(t.childCount-1,t.lastChild.copy($u(t.lastChild.content,e-1,n)))}function Tu(t,e){for(let n=0;n1&&(r=r.replaceChild(0,Du(r.firstChild,e-1,1==r.childCount?n-1:0))),e>0&&(r=t.type.contentMatch.fillBefore(r).append(r),n<=0&&(r=r.append(t.type.contentMatch.matchFragment(r).fillBefore(ec.empty,!0)))),t.copy(r)}function Nu(t,e,n,r,i){let o=t.node(e),s=i?t.indexAfter(e):t.index(e);if(s==o.childCount&&!n.compatibleContent(o.type))return null;let a=r.fillBefore(o.content,!0,s);return a&&!function(t,e,n){for(let r=n;rr){let e=i.contentMatchAt(0),n=e.fillBefore(t).append(t);t=n.append(e.matchFragment(n).fillBefore(ec.empty,!0))}return t}function Pu(t,e){let n=[];for(let r=Math.min(t.depth,e.depth);r>=0;r--){let i=t.start(r);if(ie.pos+(e.depth-r)||t.node(r).type.spec.isolating||e.node(r).type.spec.isolating)break;(i==e.start(r)||r==t.depth&&r==e.depth&&t.parent.inlineContent&&e.parent.inlineContent&&r&&e.start(r-1)==i-1)&&n.push(r)}return n}let Iu=class extends Error{};Iu=function t(e){let n=Error.call(this,e);return n.__proto__=t.prototype,n},(Iu.prototype=Object.create(Error.prototype)).constructor=Iu,Iu.prototype.name="TransformError";const Ru=Object.create(null);class zu{constructor(t,e,n){this.$anchor=t,this.$head=e,this.ranges=n||[new ju(t.min(e),t.max(e))]}get anchor(){return this.$anchor.pos}get head(){return this.$head.pos}get from(){return this.$from.pos}get to(){return this.$to.pos}get $from(){return this.ranges[0].$from}get $to(){return this.ranges[0].$to}get empty(){let t=this.ranges;for(let e=0;e=0;i--){let r=e<0?Hu(t.node(0),t.node(i),t.before(i+1),t.index(i),e,n):Hu(t.node(0),t.node(i),t.after(i+1),t.index(i)+1,e,n);if(r)return r}return null}static near(t,e=1){return this.findFrom(t,e)||this.findFrom(t,-e)||new Ju(t.node(0))}static atStart(t){return Hu(t,t,0,0,1)||new Ju(t)}static atEnd(t){return Hu(t,t,t.content.size,t.childCount,-1)||new Ju(t)}static fromJSON(t,e){if(!e||!e.type)throw new RangeError("Invalid input for Selection.fromJSON");let n=Ru[e.type];if(!n)throw new RangeError(`No selection type ${e.type} defined`);return n.fromJSON(t,e)}static jsonID(t,e){if(t in Ru)throw new RangeError("Duplicate use of selection JSON ID "+t);return Ru[t]=e,e.prototype.jsonID=t,e}getBookmark(){return Bu.between(this.$anchor,this.$head).getBookmark()}}zu.prototype.visible=!0;class ju{constructor(t,e){this.$from=t,this.$to=e}}let Fu=!1;function Lu(t){Fu||t.parent.inlineContent||(Fu=!0,console.warn("TextSelection endpoint not pointing into a node with inline content ("+t.parent.type.name+")"))}class Bu extends zu{constructor(t,e=t){Lu(t),Lu(e),super(t,e)}get $cursor(){return this.$anchor.pos==this.$head.pos?this.$head:null}map(t,e){let n=t.resolve(e.map(this.head));if(!n.parent.inlineContent)return zu.near(n);let r=t.resolve(e.map(this.anchor));return new Bu(r.parent.inlineContent?r:n,n)}replace(t,e=ac.empty){if(super.replace(t,e),e==ac.empty){let e=this.$from.marksAcross(this.$to);e&&t.ensureMarks(e)}}eq(t){return t instanceof Bu&&t.anchor==this.anchor&&t.head==this.head}getBookmark(){return new Vu(this.anchor,this.head)}toJSON(){return{type:"text",anchor:this.anchor,head:this.head}}static fromJSON(t,e){if("number"!=typeof e.anchor||"number"!=typeof e.head)throw new RangeError("Invalid input for TextSelection.fromJSON");return new Bu(t.resolve(e.anchor),t.resolve(e.head))}static create(t,e,n=e){let r=t.resolve(e);return new this(r,n==e?r:t.resolve(n))}static between(t,e,n){let r=t.pos-e.pos;if(n&&!r||(n=r>=0?1:-1),!e.parent.inlineContent){let t=zu.findFrom(e,n,!0)||zu.findFrom(e,-n,!0);if(!t)return zu.near(e,n);e=t.$head}return t.parent.inlineContent||(0==r||(t=(zu.findFrom(t,-n,!0)||zu.findFrom(t,n,!0)).$anchor).posnew Ju(t)};function Hu(t,e,n,r,i,o=!1){if(e.inlineContent)return Bu.create(t,n);for(let s=r-(i>0?0:1);i>0?s=0;s+=i){let r=e.child(s);if(r.isAtom){if(!o&&Wu.isSelectable(r))return Wu.create(t,n-(i<0?r.nodeSize:0))}else{let e=Hu(t,r,n+i,i<0?r.childCount:0,i,o);if(e)return e}n+=r.nodeSize*i}return null}function Uu(t,e,n){let r=t.steps.length-1;if(r{null==i&&(i=r)})),t.setSelection(zu.near(t.doc.resolve(i),n)))}class Yu extends class{constructor(t){this.doc=t,this.steps=[],this.docs=[],this.mapping=new lu}get before(){return this.docs.length?this.docs[0]:this.doc}step(t){let e=this.maybeStep(t);if(e.failed)throw new Iu(e.failed);return this}maybeStep(t){let e=t.apply(this.doc);return e.failed||this.addStep(t,e.doc),e}get docChanged(){return this.steps.length>0}addStep(t,e){this.docs.push(this.doc),this.steps.push(t),this.mapping.appendMap(t.getMap()),this.doc=e}replace(t,e=t,n=ac.empty){let r=Ou(this.doc,t,e,n);return r&&this.step(r),this}replaceWith(t,e,n){return this.replace(t,e,new ac(ec.from(n),0,0))}delete(t,e){return this.replace(t,e,ac.empty)}insert(t,e){return this.replaceWith(t,t,e)}replaceRange(t,e,n){return function(t,e,n,r){if(!r.size)return t.deleteRange(e,n);let i=t.doc.resolve(e),o=t.doc.resolve(n);if(_u(i,o,r))return t.step(new mu(e,n,r));let s=Pu(i,t.doc.resolve(n));0==s[s.length-1]&&s.pop();let a=-(i.depth+1);s.unshift(a);for(let f=i.depth,h=i.pos-1;f>0;f--,h--){let t=i.node(f).type.spec;if(t.defining||t.definingAsContext||t.isolating)break;s.indexOf(f)>-1?a=f:i.before(f)==h&&s.splice(1,0,-f)}let l=s.indexOf(a),c=[],u=r.openStart;for(let f=r.content,h=0;;h++){let t=f.firstChild;if(c.push(t),h==r.openStart)break;f=t.content}for(let f=u-1;f>=0;f--){let t=c[f].type,e=Au(t);if(e&&i.node(l).type!=t)u=f;else if(e||!t.isTextblock)break}for(let f=r.openStart;f>=0;f--){let e=(f+u+1)%(r.openStart+1),a=c[e];if(a)for(let c=0;c=0&&(t.replace(e,n,r),!(t.steps.length>d));f--){let t=s[f];t<0||(e=i.before(t),n=o.after(t))}}(this,t,e,n),this}replaceRangeWith(t,e,n){return function(t,e,n,r){if(!r.isInline&&e==n&&t.doc.resolve(e).parent.content.size){let i=function(t,e,n){let r=t.resolve(e);if(r.parent.canReplaceWith(r.index(),r.index(),n))return e;if(0==r.parentOffset)for(let i=r.depth-1;i>=0;i--){let t=r.index(i);if(r.node(i).canReplaceWith(t,t,n))return r.before(i+1);if(t>0)return null}if(r.parentOffset==r.parent.content.size)for(let i=r.depth-1;i>=0;i--){let t=r.indexAfter(i);if(r.node(i).canReplaceWith(t,t,n))return r.after(i+1);if(t0&&(n||r.node(e-1).canReplace(r.index(e-1),i.indexAfter(e-1))))return t.delete(r.before(e),i.after(e))}for(let s=1;s<=r.depth&&s<=i.depth;s++)if(e-r.start(s)==r.depth-s&&n>r.end(s)&&i.end(s)-n!=i.depth-s)return t.delete(r.before(s),n);t.delete(e,n)}(this,t,e),this}lift(t,e){return function(t,e,n){let{$from:r,$to:i,depth:o}=e,s=r.before(o+1),a=i.after(o+1),l=s,c=a,u=ec.empty,d=0;for(let p=o,m=!1;p>n;p--)m||r.index(p)>0?(m=!0,u=ec.from(r.node(p).copy(u)),d++):l--;let f=ec.empty,h=0;for(let p=o,m=!1;p>n;p--)m||i.after(p+1)=0;s--){if(r.size){let t=n[s].type.contentMatch.matchFragment(r);if(!t||!t.validEnd)throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")}r=ec.from(n[s].type.create(n[s].attrs,r))}let i=e.start,o=e.end;t.step(new gu(i,o,i,o,new ac(r,0,0),n.length,!0))}(this,t,e),this}setBlockType(t,e=t,n,r=null){return function(t,e,n,r,i){if(!r.isTextblock)throw new RangeError("Type given to setBlockType should be a textblock");let o=t.steps.length;t.doc.nodesBetween(e,n,((e,n)=>{if(e.isTextblock&&!e.hasMarkup(r,i)&&function(t,e,n){let r=t.resolve(e),i=r.index();return r.parent.canReplaceWith(i,i+1,n)}(t.doc,t.mapping.slice(o).map(n),r)){t.clearIncompatible(t.mapping.slice(o).map(n,1),r);let s=t.mapping.slice(o),a=s.map(n,1),l=s.map(n+e.nodeSize,1);return t.step(new gu(a,l,a+1,l-1,new ac(ec.from(r.create(i,null,e.marks)),0,0),1,!0)),!1}}))}(this,t,e,n,r),this}setNodeMarkup(t,e,n=null,r=[]){return function(t,e,n,r,i){let o=t.doc.nodeAt(e);if(!o)throw new RangeError("No node at given position");n||(n=o.type);let s=n.create(r,null,i||o.marks);if(o.isLeaf)return t.replaceWith(e,e+o.nodeSize,s);if(!n.validContent(o.content))throw new RangeError("Invalid content for node type "+n.name);t.step(new gu(e,e+o.nodeSize,e+1,e+o.nodeSize-1,new ac(ec.from(s),0,0),1,!0))}(this,t,e,n,r),this}split(t,e=1,n){return function(t,e,n=1,r){let i=t.doc.resolve(e),o=ec.empty,s=ec.empty;for(let a=i.depth,l=i.depth-n,c=n-1;a>l;a--,c--){o=ec.from(i.node(a).copy(o));let t=r&&r[c];s=ec.from(t?t.type.create(t.attrs,s):i.node(a).copy(s))}t.step(new mu(e,e,new ac(o.append(s),n,n),!0))}(this,t,e,n),this}addMark(t,e,n){return function(t,e,n,r){let i,o,s=[],a=[];t.doc.nodesBetween(e,n,((t,l,c)=>{if(!t.isInline)return;let u=t.marks;if(!r.isInSet(u)&&c.type.allowsMarkType(r.type)){let c=Math.max(l,e),d=Math.min(l+t.nodeSize,n),f=r.addToSet(u);for(let t=0;tt.step(e))),a.forEach((e=>t.step(e)))}(this,t,e,n),this}removeMark(t,e,n){return function(t,e,n,r){let i=[],o=0;t.doc.nodesBetween(e,n,((t,s)=>{if(!t.isInline)return;o++;let a=null;if(r instanceof Vc){let e,n=t.marks;for(;e=r.isInSet(n);)(a||(a=[])).push(e),n=e.removeFromSet(n)}else r?r.isInSet(t.marks)&&(a=[r]):a=t.marks;if(a&&a.length){let r=Math.min(s+t.nodeSize,n);for(let t=0;tt.step(new pu(e.from,e.to,e.style))))}(this,t,e,n),this}clearIncompatible(t,e,n){return function(t,e,n,r=n.contentMatch){let i=t.doc.nodeAt(e),o=[],s=e+1;for(let a=0;a=0;a--)t.step(o[a])}(this,t,e,n),this}}{constructor(t){super(t.doc),this.curSelectionFor=0,this.updated=0,this.meta=Object.create(null),this.time=Date.now(),this.curSelection=t.selection,this.storedMarks=t.storedMarks}get selection(){return this.curSelectionFor0}setStoredMarks(t){return this.storedMarks=t,this.updated|=2,this}ensureMarks(t){return oc.sameSet(this.storedMarks||this.selection.$from.marks(),t)||this.setStoredMarks(t),this}addStoredMark(t){return this.ensureMarks(t.addToSet(this.storedMarks||this.selection.$head.marks()))}removeStoredMark(t){return this.ensureMarks(t.removeFromSet(this.storedMarks||this.selection.$head.marks()))}get storedMarksSet(){return(2&this.updated)>0}addStep(t,e){super.addStep(t,e),this.updated=-3&this.updated,this.storedMarks=null}setTime(t){return this.time=t,this}replaceSelection(t){return this.selection.replace(this,t),this}replaceSelectionWith(t,e=!0){let n=this.selection;return e&&(t=t.mark(this.storedMarks||(n.empty?n.$from.marks():n.$from.marksAcross(n.$to)||oc.none))),n.replaceWith(this,t),this}deleteSelection(){return this.selection.replace(this),this}insertText(t,e,n){let r=this.doc.type.schema;if(null==e)return t?this.replaceSelectionWith(r.text(t),!0):this.deleteSelection();{if(null==n&&(n=e),n=null==n?e:n,!t)return this.deleteRange(e,n);let i=this.storedMarks;if(!i){let t=this.doc.resolve(e);i=n==e?t.marks():t.marksAcross(this.doc.resolve(n))}return this.replaceRangeWith(e,n,r.text(t,i)),this.selection.empty||this.setSelection(zu.near(this.selection.$to)),this}}setMeta(t,e){return this.meta["string"==typeof t?t:t.key]=e,this}getMeta(t){return this.meta["string"==typeof t?t:t.key]}get isGeneric(){for(let t in this.meta)return!1;return!0}scrollIntoView(){return this.updated|=4,this}get scrolledIntoView(){return(4&this.updated)>0}}function Gu(t,e){return e&&t?t.bind(e):t}class Zu{constructor(t,e,n){this.name=t,this.init=Gu(e.init,n),this.apply=Gu(e.apply,n)}}const Xu=[new Zu("doc",{init:t=>t.doc||t.schema.topNodeType.createAndFill(),apply:t=>t.doc}),new Zu("selection",{init:(t,e)=>t.selection||zu.atStart(e.doc),apply:t=>t.selection}),new Zu("storedMarks",{init:t=>t.storedMarks||null,apply:(t,e,n,r)=>r.selection.$cursor?t.storedMarks:null}),new Zu("scrollToSelection",{init:()=>0,apply:(t,e)=>t.scrolledIntoView?e+1:e})];class Qu{constructor(t,e){this.schema=t,this.plugins=[],this.pluginsByKey=Object.create(null),this.fields=Xu.slice(),e&&e.forEach((t=>{if(this.pluginsByKey[t.key])throw new RangeError("Adding different instances of a keyed plugin ("+t.key+")");this.plugins.push(t),this.pluginsByKey[t.key]=t,t.spec.state&&this.fields.push(new Zu(t.key,t.spec.state,t))}))}}class td{constructor(t){this.config=t}get schema(){return this.config.schema}get plugins(){return this.config.plugins}apply(t){return this.applyTransaction(t).state}filterTransaction(t,e=-1){for(let n=0;nt.toJSON()))),t&&"object"==typeof t)for(let n in t){if("doc"==n||"selection"==n)throw new RangeError("The JSON fields `doc` and `selection` are reserved");let r=t[n],i=r.spec.state;i&&i.toJSON&&(e[n]=i.toJSON.call(r,this[r.key]))}return e}static fromJSON(t,e,n){if(!e)throw new RangeError("Invalid input for EditorState.fromJSON");if(!t.schema)throw new RangeError("Required config field 'schema' missing");let r=new Qu(t.schema,t.plugins),i=new td(r);return r.fields.forEach((r=>{if("doc"==r.name)i.doc=_c.fromJSON(t.schema,e.doc);else if("selection"==r.name)i.selection=zu.fromJSON(i.doc,e.selection);else if("storedMarks"==r.name)e.storedMarks&&(i.storedMarks=e.storedMarks.map(t.schema.markFromJSON));else{if(n)for(let o in n){let s=n[o],a=s.spec.state;if(s.key==r.name&&a&&a.fromJSON&&Object.prototype.hasOwnProperty.call(e,o))return void(i[r.name]=a.fromJSON.call(s,t,e[o],i))}i[r.name]=r.init(t,i)}})),i}}function ed(t,e,n){for(let r in t){let i=t[r];i instanceof Function?i=i.bind(e):"handleDOMEvents"==r&&(i=ed(i,e,{})),n[r]=i}return n}class nd{constructor(t){this.spec=t,this.props={},t.props&&ed(t.props,this,this.props),this.key=t.key?t.key.key:id("plugin")}getState(t){return t[this.key]}}const rd=Object.create(null);function id(t){return t in rd?t+"$"+ ++rd[t]:(rd[t]=0,t+"$")}class od{constructor(t="key"){this.key=id(t)}get(t){return t.config.pluginsByKey[this.key]}getState(t){return t[this.key]}}const sd="undefined"!=typeof navigator?navigator:null,ad="undefined"!=typeof document?document:null,ld=sd&&sd.userAgent||"",cd=/Edge\/(\d+)/.exec(ld),ud=/MSIE \d/.exec(ld),dd=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(ld),fd=!!(ud||dd||cd),hd=ud?document.documentMode:dd?+dd[1]:cd?+cd[1]:0,pd=!fd&&/gecko\/(\d+)/i.test(ld);pd&&(/Firefox\/(\d+)/.exec(ld)||[0,0])[1];const md=!fd&&/Chrome\/(\d+)/.exec(ld),gd=!!md,vd=md?+md[1]:0,yd=!fd&&!!sd&&/Apple Computer/.test(sd.vendor),bd=yd&&(/Mobile\/\w+/.test(ld)||!!sd&&sd.maxTouchPoints>2),wd=bd||!!sd&&/Mac/.test(sd.platform),xd=/Android \d/.test(ld),Sd=!!ad&&"webkitFontSmoothing"in ad.documentElement.style,kd=Sd?+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]:0,Od=function(t){for(var e=0;;e++)if(!(t=t.previousSibling))return e},_d=function(t){let e=t.assignedSlot||t.parentNode;return e&&11==e.nodeType?e.host:e};let Md=null;const Cd=function(t,e,n){let r=Md||(Md=document.createRange());return r.setEnd(t,null==n?t.nodeValue.length:n),r.setStart(t,e||0),r},$d=function(t,e,n,r){return n&&(Dd(t,e,n,r,-1)||Dd(t,e,n,r,1))},Td=/^(img|br|input|textarea|hr)$/i;function Dd(t,e,n,r,i){for(;;){if(t==n&&e==r)return!0;if(e==(i<0?0:Nd(t))){let n=t.parentNode;if(!n||1!=n.nodeType||Ad(t)||Td.test(t.nodeName)||"false"==t.contentEditable)return!1;e=Od(t)+(i<0?0:1),t=n}else{if(1!=t.nodeType)return!1;if("false"==(t=t.childNodes[e+(i<0?-1:0)]).contentEditable)return!1;e=i<0?Nd(t):0}}}function Nd(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}function Ad(t){let e;for(let n=t;n&&!(e=n.pmViewDesc);n=n.parentNode);return e&&e.node&&e.node.isBlock&&(e.dom==t||e.contentDOM==t)}const Ed=function(t){let e=t.isCollapsed;return e&&gd&&t.rangeCount&&!t.getRangeAt(0).collapsed&&(e=!1),e};function Pd(t,e){let n=document.createEvent("Event");return n.initEvent("keydown",!0,!0),n.keyCode=t,n.key=n.code=e,n}function Id(t){return{left:0,right:t.documentElement.clientWidth,top:0,bottom:t.documentElement.clientHeight}}function Rd(t,e){return"number"==typeof t?t:t[e]}function zd(t){let e=t.getBoundingClientRect(),n=e.width/t.offsetWidth||1,r=e.height/t.offsetHeight||1;return{left:e.left,right:e.left+t.clientWidth*n,top:e.top,bottom:e.top+t.clientHeight*r}}function jd(t,e,n){let r=t.someProp("scrollThreshold")||0,i=t.someProp("scrollMargin")||5,o=t.dom.ownerDocument;for(let s=n||t.dom;s;s=_d(s)){if(1!=s.nodeType)continue;let t=s,n=t==o.body,a=n?Id(o):zd(t),l=0,c=0;if(e.topa.bottom-Rd(r,"bottom")&&(c=e.bottom-a.bottom+Rd(i,"bottom")),e.lefta.right-Rd(r,"right")&&(l=e.right-a.right+Rd(i,"right")),l||c)if(n)o.defaultView.scrollBy(l,c);else{let n=t.scrollLeft,r=t.scrollTop;c&&(t.scrollTop+=c),l&&(t.scrollLeft+=l);let i=t.scrollLeft-n,o=t.scrollTop-r;e={left:e.left-i,top:e.top-o,right:e.right-i,bottom:e.bottom-o}}if(n)break}}function Fd(t){let e=[],n=t.ownerDocument;for(let r=t;r&&(e.push({dom:r,top:r.scrollTop,left:r.scrollLeft}),t!=n);r=_d(r));return e}function Ld(t,e){for(let n=0;n=a){s=Math.max(d.bottom,s),a=Math.min(d.top,a);let t=d.left>e.left?d.left-e.left:d.right=(d.left+d.right)/2?1:0));continue}}!n&&(e.left>=d.right&&e.top>=d.top||e.left>=d.left&&e.top>=d.bottom)&&(o=c+1)}}return n&&3==n.nodeType?function(t,e){let n=t.nodeValue.length,r=document.createRange();for(let i=0;i=(n.left+n.right)/2?1:0)}}return{node:t,offset:0}}(n,r):!n||i&&1==n.nodeType?{node:t,offset:o}:Vd(n,r)}function Wd(t,e){return t.left>=e.left-1&&t.left<=e.right+1&&t.top>=e.top-1&&t.top<=e.bottom+1}function qd(t,e,n){let r=t.childNodes.length;if(r&&n.tope.top&&i++}n==t.dom&&i==n.childNodes.length-1&&1==n.lastChild.nodeType&&e.top>n.lastChild.getBoundingClientRect().bottom?o=t.state.doc.content.size:0!=i&&1==n.nodeType&&"BR"==n.childNodes[i-1].nodeName||(o=function(t,e,n,r){let i=-1;for(let o=e;o!=t.dom;){let e=t.docView.nearestDesc(o,!0);if(!e)return null;if(e.node.isBlock&&e.parent){let t=e.dom.getBoundingClientRect();if(t.left>r.left||t.top>r.top)i=e.posBefore;else{if(!(t.right-1?i:t.docView.posFromDOM(e,n,1)}(t,n,i,e))}null==o&&(o=function(t,e,n){let{node:r,offset:i}=Vd(e,n),o=-1;if(1==r.nodeType&&!r.firstChild){let t=r.getBoundingClientRect();o=t.left!=t.right&&n.left>(t.left+t.right)/2?1:-1}return t.docView.posFromDOM(r,i,o)}(t,s,e));let a=t.docView.nearestDesc(s,!0);return{pos:o,inside:a?a.posAtStart-a.border:-1}}function Kd(t,e){let n=t.getClientRects();return n.length?n[e<0?0:n.length-1]:t.getBoundingClientRect()}const Hd=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/;function Ud(t,e,n){let{node:r,offset:i,atom:o}=t.docView.domFromPos(e,n<0?-1:1),s=Sd||pd;if(3==r.nodeType){if(!s||!Hd.test(r.nodeValue)&&(n<0?i:i!=r.nodeValue.length)){let t=i,e=i,o=n<0?1:-1;return n<0&&!i?(e++,o=-1):n>=0&&i==r.nodeValue.length?(t--,o=1):n<0?t--:e++,Yd(Kd(Cd(r,t,e),o),o<0)}{let t=Kd(Cd(r,i,i),n);if(pd&&i&&/\s/.test(r.nodeValue[i-1])&&i=0)}if(null==o&&i&&(n<0||i==Nd(r))){let t=r.childNodes[i-1],e=3==t.nodeType?Cd(t,Nd(t)-(s?0:1)):1!=t.nodeType||"BR"==t.nodeName&&t.nextSibling?null:t;if(e)return Yd(Kd(e,1),!1)}if(null==o&&i=0)}function Yd(t,e){if(0==t.width)return t;let n=e?t.left:t.right;return{top:t.top,bottom:t.bottom,left:n,right:n}}function Gd(t,e){if(0==t.height)return t;let n=e?t.top:t.bottom;return{top:n,bottom:n,left:t.left,right:t.right}}function Zd(t,e,n){let r=t.state,i=t.root.activeElement;r!=e&&t.updateState(e),i!=t.dom&&t.focus();try{return n()}finally{r!=e&&t.updateState(r),i!=t.dom&&i&&i.focus()}}const Xd=/[\u0590-\u08ac]/;let Qd=null,tf=null,ef=!1;function nf(t,e,n){return Qd==e&&tf==n?ef:(Qd=e,tf=n,ef="up"==n||"down"==n?function(t,e,n){let r=e.selection,i="up"==n?r.$from:r.$to;return Zd(t,e,(()=>{let{node:e}=t.docView.domFromPos(i.pos,"up"==n?-1:1);for(;;){let n=t.docView.nearestDesc(e,!0);if(!n)break;if(n.node.isBlock){e=n.dom;break}e=n.dom.parentNode}let r=Ud(t,i.pos,1);for(let t=e.firstChild;t;t=t.nextSibling){let e;if(1==t.nodeType)e=t.getClientRects();else{if(3!=t.nodeType)continue;e=Cd(t,0,t.nodeValue.length).getClientRects()}for(let t=0;ti.top+1&&("up"==n?r.top-i.top>2*(i.bottom-r.top):i.bottom-r.bottom>2*(r.bottom-i.top)))return!1}}return!0}))}(t,e,n):function(t,e,n){let{$head:r}=e.selection;if(!r.parent.isTextblock)return!1;let i=r.parentOffset,o=!i,s=i==r.parent.content.size,a=t.domSelection();return Xd.test(r.parent.textContent)&&a.modify?Zd(t,e,(()=>{let e=a.getRangeAt(0),i=a.focusNode,o=a.focusOffset,s=a.caretBidiLevel;a.modify("move",n,"character");let l=!(r.depth?t.docView.domAfterPos(r.before()):t.dom).contains(1==a.focusNode.nodeType?a.focusNode:a.focusNode.parentNode)||i==a.focusNode&&o==a.focusOffset;return a.removeAllRanges(),a.addRange(e),null!=s&&(a.caretBidiLevel=s),l})):"left"==n||"backward"==n?o:s}(t,e,n))}class rf{constructor(t,e,n,r){this.parent=t,this.children=e,this.dom=n,this.contentDOM=r,this.dirty=0,n.pmViewDesc=this}matchesWidget(t){return!1}matchesMark(t){return!1}matchesNode(t,e,n){return!1}matchesHack(t){return!1}parseRule(){return null}stopEvent(t){return!1}get size(){let t=0;for(let e=0;eOd(this.contentDOM);else if(this.contentDOM&&this.contentDOM!=this.dom&&this.dom.contains(this.contentDOM))r=2&t.compareDocumentPosition(this.contentDOM);else if(this.dom.firstChild){if(0==e)for(let e=t;;e=e.parentNode){if(e==this.dom){r=!1;break}if(e.previousSibling)break}if(null==r&&e==t.childNodes.length)for(let e=t;;e=e.parentNode){if(e==this.dom){r=!0;break}if(e.nextSibling)break}}return(null==r?n>0:r)?this.posAtEnd:this.posAtStart}nearestDesc(t,e=!1){for(let n=!0,r=t;r;r=r.parentNode){let i,o=this.getDesc(r);if(o&&(!e||o.node)){if(!n||!(i=o.nodeDOM)||(1==i.nodeType?i.contains(1==t.nodeType?t:t.parentNode):i==t))return o;n=!1}}}getDesc(t){let e=t.pmViewDesc;for(let n=e;n;n=n.parent)if(n==this)return e}posFromDOM(t,e,n){for(let r=t;r;r=r.parentNode){let i=this.getDesc(r);if(i)return i.localPosFromDOM(t,e,n)}return-1}descAt(t){for(let e=0,n=0;et||e instanceof df){i=t-o;break}o=n}if(i)return this.children[r].domFromPos(i-this.children[r].border,e);for(;r&&!(n=this.children[r-1]).size&&n instanceof of&&n.side>=0;r--);if(e<=0){let t,n=!0;for(;t=r?this.children[r-1]:null,t&&t.dom.parentNode!=this.contentDOM;r--,n=!1);return t&&e&&n&&!t.border&&!t.domAtom?t.domFromPos(t.size,e):{node:this.contentDOM,offset:t?Od(t.dom)+1:0}}{let t,n=!0;for(;t=r=i&&e<=a-n.border&&n.node&&n.contentDOM&&this.contentDOM.contains(n.contentDOM))return n.parseRange(t,e,i);t=o;for(let e=s;e>0;e--){let n=this.children[e-1];if(n.size&&n.dom.parentNode==this.contentDOM&&!n.emptyChildAt(1)){r=Od(n.dom)+1;break}t-=n.size}-1==r&&(r=0)}if(r>-1&&(a>e||s==this.children.length-1)){e=a;for(let t=s+1;th&&oe){let t=s;s=a,a=t}let n=document.createRange();n.setEnd(a.node,a.offset),n.setStart(s.node,s.offset),l.removeAllRanges(),l.addRange(n)}}ignoreMutation(t){return!this.contentDOM&&"selection"!=t.type}get contentLost(){return this.contentDOM&&this.contentDOM!=this.dom&&!this.dom.contains(this.contentDOM)}markDirty(t,e){for(let n=0,r=0;r=n:tn){let r=n+i.border,s=o-i.border;if(t>=r&&e<=s)return this.dirty=t==n||e==o?2:1,void(t!=r||e!=s||!i.contentLost&&i.dom.parentNode==this.contentDOM?i.markDirty(t-r,e-r):i.dirty=3);i.dirty=i.dom!=i.contentDOM||i.dom.parentNode!=this.contentDOM||i.children.length?3:2}n=o}this.dirty=2}markParentsDirty(){let t=1;for(let e=this.parent;e;e=e.parent,t++){let n=1==t?2:1;e.dirtyi?i.parent?i.parent.posBeforeChild(i):void 0:r))),!e.type.spec.raw){if(1!=o.nodeType){let t=document.createElement("span");t.appendChild(o),o=t}o.contentEditable="false",o.classList.add("ProseMirror-widget")}super(t,[],o,null),this.widget=e,this.widget=e,i=this}matchesWidget(t){return 0==this.dirty&&t.type.eq(this.widget.type)}parseRule(){return{ignore:!0}}stopEvent(t){let e=this.widget.spec.stopEvent;return!!e&&e(t)}ignoreMutation(t){return"selection"!=t.type||this.widget.spec.ignoreSelection}destroy(){this.widget.type.destroy(this.dom),super.destroy()}get domAtom(){return!0}get side(){return this.widget.type.side}}class sf extends rf{constructor(t,e,n,r){super(t,[],e,null),this.textDOM=n,this.text=r}get size(){return this.text.length}localPosFromDOM(t,e){return t!=this.textDOM?this.posAtStart+(e?this.size:0):this.posAtStart+e}domFromPos(t){return{node:this.textDOM,offset:t}}ignoreMutation(t){return"characterData"===t.type&&t.target.nodeValue==t.oldValue}}class af extends rf{constructor(t,e,n,r){super(t,[],n,r),this.mark=e}static create(t,e,n,r){let i=r.nodeViews[e.type.name],o=i&&i(e,r,n);return o&&o.dom||(o=eu.renderSpec(document,e.type.spec.toDOM(e,n))),new af(t,e,o.dom,o.contentDOM||o.dom)}parseRule(){return 3&this.dirty||this.mark.type.spec.reparseInView?null:{mark:this.mark.type.name,attrs:this.mark.attrs,contentElement:this.contentDOM||void 0}}matchesMark(t){return 3!=this.dirty&&this.mark.eq(t)}markDirty(t,e){if(super.markDirty(t,e),0!=this.dirty){let t=this.parent;for(;!t.node;)t=t.parent;t.dirty0&&(i=Of(i,0,t,n));for(let s=0;ss?s.parent?s.parent.posBeforeChild(s):void 0:o),n,r),c=l&&l.dom,u=l&&l.contentDOM;if(e.isText)if(c){if(3!=c.nodeType)throw new RangeError("Text must be rendered as a DOM text node")}else c=document.createTextNode(e.text);else c||({dom:c,contentDOM:u}=eu.renderSpec(document,e.type.spec.toDOM(e)));u||e.isText||"BR"==c.nodeName||(c.hasAttribute("contenteditable")||(c.contentEditable="false"),e.type.spec.draggable&&(c.draggable=!0));let d=c;return c=bf(c,n,e),l?s=new ff(t,e,n,r,c,u||null,d,l,i,o+1):e.isText?new uf(t,e,n,r,c,d,i):new lf(t,e,n,r,c,u||null,d,i,o+1)}parseRule(){if(this.node.type.spec.reparseInView)return null;let t={node:this.node.type.name,attrs:this.node.attrs};if("pre"==this.node.type.whitespace&&(t.preserveWhitespace="full"),this.contentDOM)if(this.contentLost){for(let e=this.children.length-1;e>=0;e--){let n=this.children[e];if(this.dom.contains(n.dom.parentNode)){t.contentElement=n.dom.parentNode;break}}t.contentElement||(t.getContent=()=>ec.empty)}else t.contentElement=this.contentDOM;else t.getContent=()=>this.node.content;return t}matchesNode(t,e,n){return 0==this.dirty&&t.eq(this.node)&&wf(e,this.outerDeco)&&n.eq(this.innerDeco)}get size(){return this.node.nodeSize}get border(){return this.node.isLeaf?0:1}updateChildren(t,e){let n=this.node.inlineContent,r=e,i=t.composing?this.localCompositionInfo(t,e):null,o=i&&i.pos>-1?i:null,s=i&&i.pos<0,a=new Sf(this,o&&o.node);!function(t,e,n,r){let i=e.locals(t),o=0;if(0==i.length){for(let n=0;no;)a.push(i[s++]);let f=o+u.nodeSize;if(u.isText){let t=f;s!t.inline)):a.slice(),e.forChild(o,u),d),o=f}}(this.node,this.innerDeco,((e,i,o)=>{e.spec.marks?a.syncToMarks(e.spec.marks,n,t):e.type.side>=0&&!o&&a.syncToMarks(i==this.node.childCount?oc.none:this.node.child(i).marks,n,t),a.placeWidget(e,t,r)}),((e,o,l,c)=>{let u;a.syncToMarks(e.marks,n,t),a.findNodeMatch(e,o,l,c)||s&&t.state.selection.from>r&&t.state.selection.to-1&&a.updateNodeAt(e,o,l,u,t)||a.updateNextNode(e,o,l,t,c)||a.addNode(e,o,l,t,r),r+=e.nodeSize})),a.syncToMarks([],n,t),this.node.isTextblock&&a.addTextblockHacks(),a.destroyRest(),(a.changed||2==this.dirty)&&(o&&this.protectLocalComposition(t,o),hf(this.contentDOM,this.children,t),bd&&function(t){if("UL"==t.nodeName||"OL"==t.nodeName){let e=t.style.cssText;t.style.cssText=e+"; list-style: square !important",window.getComputedStyle(t).listStyle,t.style.cssText=e}}(this.dom))}localCompositionInfo(t,e){let{from:n,to:r}=t.state.selection;if(!(t.state.selection instanceof Bu)||ne+this.node.content.size)return null;let i=t.domSelection(),o=function(t,e){for(;;){if(3==t.nodeType)return t;if(1==t.nodeType&&e>0){if(t.childNodes.length>e&&3==t.childNodes[e].nodeType)return t.childNodes[e];e=Nd(t=t.childNodes[e-1])}else{if(!(1==t.nodeType&&e=n){let t=a=0&&t+e.length+a>=n)return a+t;if(n==r&&l.length>=r+e.length-a&&l.slice(r-a,r-a+e.length)==e)return r}}return-1}(this.node.content,t,n-e,r-e);return i<0?null:{node:o,pos:i,text:t}}return{node:o,pos:-1,text:""}}protectLocalComposition(t,{node:e,pos:n,text:r}){if(this.getDesc(e))return;let i=e;for(;i.parentNode!=this.contentDOM;i=i.parentNode){for(;i.previousSibling;)i.parentNode.removeChild(i.previousSibling);for(;i.nextSibling;)i.parentNode.removeChild(i.nextSibling);i.pmViewDesc&&(i.pmViewDesc=void 0)}let o=new sf(this,i,e,r);t.input.compositionNodes.push(o),this.children=Of(this.children,n,n+r.length,t,o)}update(t,e,n,r){return!(3==this.dirty||!t.sameMarkup(this.node))&&(this.updateInner(t,e,n,r),!0)}updateInner(t,e,n,r){this.updateOuterDeco(e),this.node=t,this.innerDeco=n,this.contentDOM&&this.updateChildren(r,this.posAtStart),this.dirty=0}updateOuterDeco(t){if(wf(t,this.outerDeco))return;let e=1!=this.nodeDOM.nodeType,n=this.dom;this.dom=vf(this.dom,this.nodeDOM,gf(this.outerDeco,this.node,e),gf(t,this.node,e)),this.dom!=n&&(n.pmViewDesc=void 0,this.dom.pmViewDesc=this),this.outerDeco=t}selectNode(){1==this.nodeDOM.nodeType&&this.nodeDOM.classList.add("ProseMirror-selectednode"),!this.contentDOM&&this.node.type.spec.draggable||(this.dom.draggable=!0)}deselectNode(){1==this.nodeDOM.nodeType&&this.nodeDOM.classList.remove("ProseMirror-selectednode"),!this.contentDOM&&this.node.type.spec.draggable||this.dom.removeAttribute("draggable")}get domAtom(){return this.node.isAtom}}function cf(t,e,n,r,i){return bf(r,e,t),new lf(void 0,t,e,n,r,r,r,i,0)}class uf extends lf{constructor(t,e,n,r,i,o,s){super(t,e,n,r,i,null,o,s,0)}parseRule(){let t=this.nodeDOM.parentNode;for(;t&&t!=this.dom&&!t.pmIsDeco;)t=t.parentNode;return{skip:t||!0}}update(t,e,n,r){return!(3==this.dirty||0!=this.dirty&&!this.inParent()||!t.sameMarkup(this.node))&&(this.updateOuterDeco(e),0==this.dirty&&t.text==this.node.text||t.text==this.nodeDOM.nodeValue||(this.nodeDOM.nodeValue=t.text,r.trackWrites==this.nodeDOM&&(r.trackWrites=null)),this.node=t,this.dirty=0,!0)}inParent(){let t=this.parent.contentDOM;for(let e=this.nodeDOM;e;e=e.parentNode)if(e==t)return!0;return!1}domFromPos(t){return{node:this.nodeDOM,offset:t}}localPosFromDOM(t,e,n){return t==this.nodeDOM?this.posAtStart+Math.min(e,this.node.text.length):super.localPosFromDOM(t,e,n)}ignoreMutation(t){return"characterData"!=t.type&&"selection"!=t.type}slice(t,e,n){let r=this.node.cut(t,e),i=document.createTextNode(r.text);return new uf(this.parent,r,this.outerDeco,this.innerDeco,i,i,n)}markDirty(t,e){super.markDirty(t,e),this.dom==this.nodeDOM||0!=t&&e!=this.nodeDOM.nodeValue.length||(this.dirty=3)}get domAtom(){return!1}}class df extends rf{parseRule(){return{ignore:!0}}matchesHack(t){return 0==this.dirty&&this.dom.nodeName==t}get domAtom(){return!0}get ignoreForCoords(){return"IMG"==this.dom.nodeName}}class ff extends lf{constructor(t,e,n,r,i,o,s,a,l,c){super(t,e,n,r,i,o,s,l,c),this.spec=a}update(t,e,n,r){if(3==this.dirty)return!1;if(this.spec.update){let i=this.spec.update(t,e,n);return i&&this.updateInner(t,e,n,r),i}return!(!this.contentDOM&&!t.isLeaf)&&super.update(t,e,n,r)}selectNode(){this.spec.selectNode?this.spec.selectNode():super.selectNode()}deselectNode(){this.spec.deselectNode?this.spec.deselectNode():super.deselectNode()}setSelection(t,e,n,r){this.spec.setSelection?this.spec.setSelection(t,e,n):super.setSelection(t,e,n,r)}destroy(){this.spec.destroy&&this.spec.destroy(),super.destroy()}stopEvent(t){return!!this.spec.stopEvent&&this.spec.stopEvent(t)}ignoreMutation(t){return this.spec.ignoreMutation?this.spec.ignoreMutation(t):super.ignoreMutation(t)}}function hf(t,e,n){let r=t.firstChild,i=!1;for(let o=0;o0;){let a;for(;;)if(r){let t=n.children[r-1];if(!(t instanceof af)){a=t,r--;break}n=t,r=t.children.length}else{if(n==e)break t;r=n.parent.children.indexOf(n),n=n.parent}let l=a.node;if(l){if(l!=t.child(i-1))break;--i,o.set(a,i),s.push(a)}}return{index:i,matched:o,matches:s.reverse()}}(t.node.content,t)}destroyBetween(t,e){if(t!=e){for(let n=t;n>1,o=Math.min(i,t.length);for(;r-1)r>this.index&&(this.changed=!0,this.destroyBetween(this.index,r)),this.top=this.top.children[this.index];else{let r=af.create(this.top,t[i],e,n);this.top.children.splice(this.index,0,r),this.top=r,this.changed=!0}this.index=0,i++}}findNodeMatch(t,e,n,r){let i,o=-1;if(r>=this.preMatch.index&&(i=this.preMatch.matches[r-this.preMatch.index]).parent==this.top&&i.matchesNode(t,e,n))o=this.top.children.indexOf(i,this.index);else for(let s=this.index,a=Math.min(this.top.children.length,s+5);s=n||u<=e?o.push(l):(cn&&o.push(l.slice(n-c,l.size,r)))}return o}function _f(t,e=null){let n=t.domSelection(),r=t.state.doc;if(!n.focusNode)return null;let i=t.docView.nearestDesc(n.focusNode),o=i&&0==i.size,s=t.docView.posFromDOM(n.focusNode,n.focusOffset,1);if(s<0)return null;let a,l,c=r.resolve(s);if(Ed(n)){for(a=c;i&&!i.node;)i=i.parent;let t=i.node;if(i&&t.isAtom&&Wu.isSelectable(t)&&i.parent&&(!t.isInline||!function(t,e,n){for(let r=0==e,i=e==Nd(t);r||i;){if(t==n)return!0;let e=Od(t);if(!(t=t.parentNode))return!1;r=r&&0==e,i=i&&e==Nd(t)}}(n.focusNode,n.focusOffset,i.dom))){let t=i.posBefore;l=new Wu(s==t?c:r.resolve(t))}}else{let e=t.docView.posFromDOM(n.anchorNode,n.anchorOffset,1);if(e<0)return null;a=r.resolve(e)}if(!l){l=Pf(t,a,c,"pointer"==e||t.state.selection.head{n.anchorNode==r&&n.anchorOffset==i||(e.removeEventListener("selectionchange",t.input.hideSelectionGuard),setTimeout((()=>{Mf(t)&&!t.state.selection.visible||t.dom.classList.remove("ProseMirror-hideselection")}),20))})}(t))}t.domObserver.setCurSelection(),t.domObserver.connectSelection()}}const $f=yd||gd&&vd<63;function Tf(t,e){let{node:n,offset:r}=t.docView.domFromPos(e,0),i=rr(t,e,n)))||Bu.between(e,n,r)}function If(t){return(!t.editable||t.root.activeElement==t.dom)&&Rf(t)}function Rf(t){let e=t.domSelection();if(!e.anchorNode)return!1;try{return t.dom.contains(3==e.anchorNode.nodeType?e.anchorNode.parentNode:e.anchorNode)&&(t.editable||t.dom.contains(3==e.focusNode.nodeType?e.focusNode.parentNode:e.focusNode))}catch(n){return!1}}function zf(t,e){let{$anchor:n,$head:r}=t.selection,i=e>0?n.max(r):n.min(r),o=i.parent.inlineContent?i.depth?t.doc.resolve(e>0?i.after():i.before()):null:i;return o&&zu.findFrom(o,e)}function jf(t,e){return t.dispatch(t.state.tr.setSelection(e).scrollIntoView()),!0}function Ff(t,e,n){let r=t.state.selection;if(!(r instanceof Bu)){if(r instanceof Wu&&r.node.isInline)return jf(t,new Bu(e>0?r.$to:r.$from));{let n=zf(t.state,e);return!!n&&jf(t,n)}}if(!r.empty||n.indexOf("s")>-1)return!1;if(t.endOfTextblock(e>0?"right":"left")){let n=zf(t.state,e);return!!(n&&n instanceof Wu)&&jf(t,n)}if(!(wd&&n.indexOf("m")>-1)){let n,i=r.$head,o=i.textOffset?null:e<0?i.nodeBefore:i.nodeAfter;if(!o||o.isText)return!1;let s=e<0?i.pos-o.nodeSize:i.pos;return!!(o.isAtom||(n=t.docView.descAt(s))&&!n.contentDOM)&&(Wu.isSelectable(o)?jf(t,new Wu(e<0?t.state.doc.resolve(i.pos-o.nodeSize):i)):!!Sd&&jf(t,new Bu(t.state.doc.resolve(e<0?s:s+o.nodeSize))))}}function Lf(t){return 3==t.nodeType?t.nodeValue.length:t.childNodes.length}function Bf(t){let e=t.pmViewDesc;return e&&0==e.size&&(t.nextSibling||"BR"!=t.nodeName)}function Vf(t){let e=t.domSelection(),n=e.focusNode,r=e.focusOffset;if(!n)return;let i,o,s=!1;for(pd&&1==n.nodeType&&r0){if(1!=n.nodeType)break;{let t=n.childNodes[r-1];if(Bf(t))i=n,o=--r;else{if(3!=t.nodeType)break;n=t,r=n.nodeValue.length}}}else{if(qf(n))break;{let e=n.previousSibling;for(;e&&Bf(e);)i=n.parentNode,o=Od(e),e=e.previousSibling;if(e)n=e,r=Lf(n);else{if(n=n.parentNode,n==t.dom)break;r=0}}}s?Jf(t,e,n,r):i&&Jf(t,e,i,o)}function Wf(t){let e=t.domSelection(),n=e.focusNode,r=e.focusOffset;if(!n)return;let i,o,s=Lf(n);for(;;)if(r{t.state==i&&Cf(t)}),50)}function Kf(t,e,n){let r=t.state.selection;if(r instanceof Bu&&!r.empty||n.indexOf("s")>-1)return!1;if(wd&&n.indexOf("m")>-1)return!1;let{$from:i,$to:o}=r;if(!i.parent.inlineContent||t.endOfTextblock(e<0?"up":"down")){let n=zf(t.state,e);if(n&&n instanceof Wu)return jf(t,n)}if(!i.parent.inlineContent){let n=e<0?i:o,s=r instanceof Ju?zu.near(n,e):zu.findFrom(n,e);return!!s&&jf(t,s)}return!1}function Hf(t,e){if(!(t.state.selection instanceof Bu))return!0;let{$head:n,$anchor:r,empty:i}=t.state.selection;if(!n.sameParent(r))return!0;if(!i)return!1;if(t.endOfTextblock(e>0?"forward":"backward"))return!0;let o=!n.textOffset&&(e<0?n.nodeBefore:n.nodeAfter);if(o&&!o.isText){let r=t.state.tr;return e<0?r.delete(n.pos-o.nodeSize,n.pos):r.delete(n.pos,n.pos+o.nodeSize),t.dispatch(r),!0}return!1}function Uf(t,e,n){t.domObserver.stop(),e.contentEditable=n,t.domObserver.start()}function Yf(t,e){let n=e.keyCode,r=function(t){let e="";return t.ctrlKey&&(e+="c"),t.metaKey&&(e+="m"),t.altKey&&(e+="a"),t.shiftKey&&(e+="s"),e}(e);return 8==n||wd&&72==n&&"c"==r?Hf(t,-1)||Vf(t):46==n||wd&&68==n&&"c"==r?Hf(t,1)||Wf(t):13==n||27==n||(37==n||wd&&66==n&&"c"==r?Ff(t,-1,r)||Vf(t):39==n||wd&&70==n&&"c"==r?Ff(t,1,r)||Wf(t):38==n||wd&&80==n&&"c"==r?Kf(t,-1,r)||Vf(t):40==n||wd&&78==n&&"c"==r?function(t){if(!yd||t.state.selection.$head.parentOffset>0)return!1;let{focusNode:e,focusOffset:n}=t.domSelection();if(e&&1==e.nodeType&&0==n&&e.firstChild&&"false"==e.firstChild.contentEditable){let n=e.firstChild;Uf(t,n,"true"),setTimeout((()=>Uf(t,n,"false")),20)}return!1}(t)||Kf(t,1,r)||Wf(t):r==(wd?"m":"c")&&(66==n||73==n||89==n||90==n))}function Gf(t,e){let n=[],{content:r,openStart:i,openEnd:o}=e;for(;i>1&&o>1&&1==r.childCount&&1==r.firstChild.childCount;){i--,o--;let t=r.firstChild;n.push(t.type.name,t.attrs!=t.type.defaultAttrs?t.attrs:null),r=t.content}let s=t.someProp("clipboardSerializer")||eu.fromSchema(t.state.schema),a=sh(),l=a.createElement("div");l.appendChild(s.serializeFragment(r,{document:a}));let c,u=l.firstChild,d=0;for(;u&&1==u.nodeType&&(c=ih[u.nodeName.toLowerCase()]);){for(let t=c.length-1;t>=0;t--){let e=a.createElement(c[t]);for(;l.firstChild;)e.appendChild(l.firstChild);l.appendChild(e),d++}u=l.firstChild}return u&&1==u.nodeType&&u.setAttribute("data-pm-slice",`${i} ${o}${d?` -${d}`:""} ${JSON.stringify(n)}`),{dom:l,text:t.someProp("clipboardTextSerializer",(t=>t(e)))||e.content.textBetween(0,e.content.size,"\n\n")}}function Zf(t,e,n,r,i){let o,s,a=i.parent.type.spec.code;if(!n&&!e)return null;let l=e&&(r||a||!n);if(l){if(t.someProp("transformPastedText",(t=>{e=t(e,a||r)})),a)return e?new ac(ec.from(t.state.schema.text(e.replace(/\r\n?/g,"\n"))),0,0):ac.empty;let n=t.someProp("clipboardTextParser",(t=>t(e,i,r)));if(n)s=n;else{let n=i.marks(),{schema:r}=t.state,s=eu.fromSchema(r);o=document.createElement("div"),e.split(/(?:\r\n?|\n)+/).forEach((t=>{let e=o.appendChild(document.createElement("p"));t&&e.appendChild(s.serializeNode(r.text(t,n)))}))}}else t.someProp("transformPastedHTML",(t=>{n=t(n)})),o=function(t){let e=/^(\s*]*>)*/.exec(t);e&&(t=t.slice(e[0].length));let n,r=sh().createElement("div"),i=/<([a-z][^>\s]+)/i.exec(t);(n=i&&ih[i[1].toLowerCase()])&&(t=n.map((t=>"<"+t+">")).join("")+t+n.map((t=>"")).reverse().join(""));if(r.innerHTML=t,n)for(let o=0;o0&&o.firstChild;d--)o=o.firstChild;if(!s){let e=t.someProp("clipboardParser")||t.someProp("domParser")||Jc.fromSchema(t.state.schema);s=e.parseSlice(o,{preserveWhitespace:!(!l&&!u),context:i,ruleFromNode:t=>"BR"!=t.nodeName||t.nextSibling||!t.parentNode||Xf.test(t.parentNode.nodeName)?null:{ignore:!0}})}if(u)s=function(t,e){if(!t.size)return t;let n,r=t.content.firstChild.type.schema;try{n=JSON.parse(e)}catch(_g){return t}let{content:i,openStart:o,openEnd:s}=t;for(let a=n.length-2;a>=0;a-=2){let t=r.nodes[n[a]];if(!t||t.hasRequiredAttrs())break;i=ec.from(t.create(n[a+1],i)),o++,s++}return new ac(i,o,s)}(rh(s,+u[1],+u[2]),u[4]);else if(s=ac.maxOpen(function(t,e){if(t.childCount<2)return t;for(let n=e.depth;n>=0;n--){let r,i=e.node(n).contentMatchAt(e.index(n)),o=[];if(t.forEach((t=>{if(!o)return;let e,n=i.findWrapping(t.type);if(!n)return o=null;if(e=o.length&&r.length&&th(n,r,t,o[o.length-1],0))o[o.length-1]=e;else{o.length&&(o[o.length-1]=eh(o[o.length-1],r.length));let e=Qf(t,n);o.push(e),i=i.matchType(e.type),r=n}})),o)return ec.from(o)}return t}(s.content,i),!0),s.openStart||s.openEnd){let t=0,e=0;for(let n=s.content.firstChild;t{s=t(s)})),s}const Xf=/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;function Qf(t,e,n=0){for(let r=e.length-1;r>=n;r--)t=e[r].create(null,ec.from(t));return t}function th(t,e,n,r,i){if(i=n&&(a=e<0?s.contentMatchAt(0).fillBefore(a,t.childCount>1||o<=i).append(a):a.append(s.contentMatchAt(s.childCount).fillBefore(ec.empty,!0))),t.replaceChild(e<0?0:t.childCount-1,s.copy(a))}function rh(t,e,n){return e{for(let n in e)t.input.eventHandlers[n]||t.dom.addEventListener(n,t.input.eventHandlers[n]=e=>fh(t,e))}))}function fh(t,e){return t.someProp("handleDOMEvents",(n=>{let r=n[e.type];return!!r&&(r(t,e)||e.defaultPrevented)}))}function hh(t,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let n=e.target;n!=t.dom;n=n.parentNode)if(!n||11==n.nodeType||n.pmViewDesc&&n.pmViewDesc.stopEvent(e))return!1;return!0}function ph(t){return{left:t.clientX,top:t.clientY}}function mh(t,e,n,r,i){if(-1==r)return!1;let o=t.state.doc.resolve(r);for(let s=o.depth+1;s>0;s--)if(t.someProp(e,(e=>s>o.depth?e(t,n,o.nodeAfter,o.before(s),i,!0):e(t,n,o.node(s),o.before(s),i,!1))))return!0;return!1}function gh(t,e,n){t.focused||t.focus();let r=t.state.tr.setSelection(e);"pointer"==n&&r.setMeta("pointer",!0),t.dispatch(r)}function vh(t,e,n,r,i){return mh(t,"handleClickOn",e,n,r)||t.someProp("handleClick",(n=>n(t,e,r)))||(i?function(t,e){if(-1==e)return!1;let n,r,i=t.state.selection;i instanceof Wu&&(n=i.node);let o=t.state.doc.resolve(e);for(let s=o.depth+1;s>0;s--){let t=s>o.depth?o.nodeAfter:o.node(s);if(Wu.isSelectable(t)){r=n&&i.$from.depth>0&&s>=i.$from.depth&&o.before(i.$from.depth+1)==i.$from.pos?o.before(i.$from.depth):o.before(s);break}}return null!=r&&(gh(t,Wu.create(t.state.doc,r),"pointer"),!0)}(t,n):function(t,e){if(-1==e)return!1;let n=t.state.doc.resolve(e),r=n.nodeAfter;return!!(r&&r.isAtom&&Wu.isSelectable(r))&&(gh(t,new Wu(n),"pointer"),!0)}(t,n))}function yh(t,e,n,r){return mh(t,"handleDoubleClickOn",e,n,r)||t.someProp("handleDoubleClick",(n=>n(t,e,r)))}function bh(t,e,n,r){return mh(t,"handleTripleClickOn",e,n,r)||t.someProp("handleTripleClick",(n=>n(t,e,r)))||function(t,e,n){if(0!=n.button)return!1;let r=t.state.doc;if(-1==e)return!!r.inlineContent&&(gh(t,Bu.create(r,0,r.content.size),"pointer"),!0);let i=r.resolve(e);for(let o=i.depth+1;o>0;o--){let e=o>i.depth?i.nodeAfter:i.node(o),n=i.before(o);if(e.inlineContent)gh(t,Bu.create(r,n+1,n+1+e.content.size),"pointer");else{if(!Wu.isSelectable(e))continue;gh(t,Wu.create(r,n),"pointer")}return!0}}(t,n,r)}function wh(t){return Ch(t)}lh.keydown=(t,e)=>{let n=e;if(t.input.shiftKey=16==n.keyCode||n.shiftKey,!kh(t,n)&&(t.input.lastKeyCode=n.keyCode,t.input.lastKeyCodeTime=Date.now(),!xd||!gd||13!=n.keyCode))if(229!=n.keyCode&&t.domObserver.forceFlush(),!bd||13!=n.keyCode||n.ctrlKey||n.altKey||n.metaKey)t.someProp("handleKeyDown",(e=>e(t,n)))||Yf(t,n)?n.preventDefault():uh(t,"key");else{let e=Date.now();t.input.lastIOSEnter=e,t.input.lastIOSEnterFallbackTimeout=setTimeout((()=>{t.input.lastIOSEnter==e&&(t.someProp("handleKeyDown",(e=>e(t,Pd(13,"Enter")))),t.input.lastIOSEnter=0)}),200)}},lh.keyup=(t,e)=>{16==e.keyCode&&(t.input.shiftKey=!1)},lh.keypress=(t,e)=>{let n=e;if(kh(t,n)||!n.charCode||n.ctrlKey&&!n.altKey||wd&&n.metaKey)return;if(t.someProp("handleKeyPress",(e=>e(t,n))))return void n.preventDefault();let r=t.state.selection;if(!(r instanceof Bu&&r.$from.sameParent(r.$to))){let e=String.fromCharCode(n.charCode);t.someProp("handleTextInput",(n=>n(t,r.$from.pos,r.$to.pos,e)))||t.dispatch(t.state.tr.insertText(e).scrollIntoView()),n.preventDefault()}};const xh=wd?"metaKey":"ctrlKey";ah.mousedown=(t,e)=>{let n=e;t.input.shiftKey=n.shiftKey;let r=wh(t),i=Date.now(),o="singleClick";i-t.input.lastClick.time<500&&function(t,e){let n=e.x-t.clientX,r=e.y-t.clientY;return n*n+r*r<100}(n,t.input.lastClick)&&!n[xh]&&("singleClick"==t.input.lastClick.type?o="doubleClick":"doubleClick"==t.input.lastClick.type&&(o="tripleClick")),t.input.lastClick={time:i,x:n.clientX,y:n.clientY,type:o};let s=t.posAtCoords(ph(n));s&&("singleClick"==o?(t.input.mouseDown&&t.input.mouseDown.done(),t.input.mouseDown=new Sh(t,s,n,!!r)):("doubleClick"==o?yh:bh)(t,s.pos,s.inside,n)?n.preventDefault():uh(t,"pointer"))};class Sh{constructor(t,e,n,r){let i,o;if(this.view=t,this.pos=e,this.event=n,this.flushed=r,this.delayedSelectionSync=!1,this.mightDrag=null,this.startDoc=t.state.doc,this.selectNode=!!n[xh],this.allowDefault=n.shiftKey,e.inside>-1)i=t.state.doc.nodeAt(e.inside),o=e.inside;else{let n=t.state.doc.resolve(e.pos);i=n.parent,o=n.depth?n.before():0}const s=r?null:n.target,a=s?t.docView.nearestDesc(s,!0):null;this.target=a?a.dom:null;let{selection:l}=t.state;(0==n.button&&i.type.spec.draggable&&!1!==i.type.spec.selectable||l instanceof Wu&&l.from<=o&&l.to>o)&&(this.mightDrag={node:i,pos:o,addAttr:!(!this.target||this.target.draggable),setUneditable:!(!this.target||!pd||this.target.hasAttribute("contentEditable"))}),this.target&&this.mightDrag&&(this.mightDrag.addAttr||this.mightDrag.setUneditable)&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&(this.target.draggable=!0),this.mightDrag.setUneditable&&setTimeout((()=>{this.view.input.mouseDown==this&&this.target.setAttribute("contentEditable","false")}),20),this.view.domObserver.start()),t.root.addEventListener("mouseup",this.up=this.up.bind(this)),t.root.addEventListener("mousemove",this.move=this.move.bind(this)),uh(t,"pointer")}done(){this.view.root.removeEventListener("mouseup",this.up),this.view.root.removeEventListener("mousemove",this.move),this.mightDrag&&this.target&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&this.target.removeAttribute("draggable"),this.mightDrag.setUneditable&&this.target.removeAttribute("contentEditable"),this.view.domObserver.start()),this.delayedSelectionSync&&setTimeout((()=>Cf(this.view))),this.view.input.mouseDown=null}up(t){if(this.done(),!this.view.dom.contains(t.target))return;let e=this.pos;this.view.state.doc!=this.startDoc&&(e=this.view.posAtCoords(ph(t))),this.allowDefault||!e?uh(this.view,"pointer"):vh(this.view,e.pos,e.inside,t,this.selectNode)?t.preventDefault():0==t.button&&(this.flushed||yd&&this.mightDrag&&!this.mightDrag.node.isAtom||gd&&!(this.view.state.selection instanceof Bu)&&Math.min(Math.abs(e.pos-this.view.state.selection.from),Math.abs(e.pos-this.view.state.selection.to))<=2)?(gh(this.view,zu.near(this.view.state.doc.resolve(e.pos)),"pointer"),t.preventDefault()):uh(this.view,"pointer")}move(t){!this.allowDefault&&(Math.abs(this.event.x-t.clientX)>4||Math.abs(this.event.y-t.clientY)>4)&&(this.allowDefault=!0),uh(this.view,"pointer"),0==t.buttons&&this.done()}}function kh(t,e){return!!t.composing||!!(yd&&Math.abs(e.timeStamp-t.input.compositionEndedAt)<500)&&(t.input.compositionEndedAt=-2e8,!0)}ah.touchdown=t=>{wh(t),uh(t,"pointer")},ah.contextmenu=t=>wh(t);const Oh=xd?5e3:-1;function _h(t,e){clearTimeout(t.input.composingTimeout),e>-1&&(t.input.composingTimeout=setTimeout((()=>Ch(t)),e))}function Mh(t){for(t.composing&&(t.input.composing=!1,t.input.compositionEndedAt=function(){let t=document.createEvent("Event");return t.initEvent("event",!0,!0),t.timeStamp}());t.input.compositionNodes.length>0;)t.input.compositionNodes.pop().markParentsDirty()}function Ch(t,e=!1){if(!(xd&&t.domObserver.flushingSoon>=0)){if(t.domObserver.forceFlush(),Mh(t),e||t.docView&&t.docView.dirty){let e=_f(t);return e&&!e.eq(t.state.selection)?t.dispatch(t.state.tr.setSelection(e)):t.updateState(t.state),!0}return!1}}lh.compositionstart=lh.compositionupdate=t=>{if(!t.composing){t.domObserver.flush();let{state:e}=t,n=e.selection.$from;if(e.selection.empty&&(e.storedMarks||!n.textOffset&&n.parentOffset&&n.nodeBefore.marks.some((t=>!1===t.type.spec.inclusive))))t.markCursor=t.state.storedMarks||n.marks(),Ch(t,!0),t.markCursor=null;else if(Ch(t),pd&&e.selection.empty&&n.parentOffset&&!n.textOffset&&n.nodeBefore.marks.length){let e=t.domSelection();for(let t=e.focusNode,n=e.focusOffset;t&&1==t.nodeType&&0!=n;){let r=n<0?t.lastChild:t.childNodes[n-1];if(!r)break;if(3==r.nodeType){e.collapse(r,r.nodeValue.length);break}t=r,n=-1}}t.input.composing=!0}_h(t,Oh)},lh.compositionend=(t,e)=>{t.composing&&(t.input.composing=!1,t.input.compositionEndedAt=e.timeStamp,_h(t,20))};const $h=fd&&hd<15||bd&&kd<604;function Th(t,e,n,r){let i=Zf(t,e,n,t.input.shiftKey,t.state.selection.$from);if(t.someProp("handlePaste",(e=>e(t,r,i||ac.empty))))return!0;if(!i)return!1;let o=function(t){return 0==t.openStart&&0==t.openEnd&&1==t.content.childCount?t.content.firstChild:null}(i),s=o?t.state.tr.replaceSelectionWith(o,t.input.shiftKey):t.state.tr.replaceSelection(i);return t.dispatch(s.scrollIntoView().setMeta("paste",!0).setMeta("uiEvent","paste")),!0}ah.copy=lh.cut=(t,e)=>{let n=e,r=t.state.selection,i="cut"==n.type;if(r.empty)return;let o=$h?null:n.clipboardData,s=r.content(),{dom:a,text:l}=Gf(t,s);o?(n.preventDefault(),o.clearData(),o.setData("text/html",a.innerHTML),o.setData("text/plain",l)):function(t,e){if(!t.dom.parentNode)return;let n=t.dom.parentNode.appendChild(document.createElement("div"));n.appendChild(e),n.style.cssText="position: fixed; left: -10000px; top: 10px";let r=getSelection(),i=document.createRange();i.selectNodeContents(e),t.dom.blur(),r.removeAllRanges(),r.addRange(i),setTimeout((()=>{n.parentNode&&n.parentNode.removeChild(n),t.focus()}),50)}(t,a),i&&t.dispatch(t.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent","cut"))},lh.paste=(t,e)=>{let n=e;if(t.composing&&!xd)return;let r=$h?null:n.clipboardData;r&&Th(t,r.getData("text/plain"),r.getData("text/html"),n)?n.preventDefault():function(t,e){if(!t.dom.parentNode)return;let n=t.input.shiftKey||t.state.selection.$from.parent.type.spec.code,r=t.dom.parentNode.appendChild(document.createElement(n?"textarea":"div"));n||(r.contentEditable="true"),r.style.cssText="position: fixed; left: -10000px; top: 10px",r.focus(),setTimeout((()=>{t.focus(),r.parentNode&&r.parentNode.removeChild(r),n?Th(t,r.value,null,e):Th(t,r.textContent,r.innerHTML,e)}),50)}(t,n)};class Dh{constructor(t,e){this.slice=t,this.move=e}}const Nh=wd?"altKey":"ctrlKey";ah.dragstart=(t,e)=>{let n=e,r=t.input.mouseDown;if(r&&r.done(),!n.dataTransfer)return;let i=t.state.selection,o=i.empty?null:t.posAtCoords(ph(n));if(o&&o.pos>=i.from&&o.pos<=(i instanceof Wu?i.to-1:i.to));else if(r&&r.mightDrag)t.dispatch(t.state.tr.setSelection(Wu.create(t.state.doc,r.mightDrag.pos)));else if(n.target&&1==n.target.nodeType){let e=t.docView.nearestDesc(n.target,!0);e&&e.node.type.spec.draggable&&e!=t.docView&&t.dispatch(t.state.tr.setSelection(Wu.create(t.state.doc,e.posBefore)))}let s=t.state.selection.content(),{dom:a,text:l}=Gf(t,s);n.dataTransfer.clearData(),n.dataTransfer.setData($h?"Text":"text/html",a.innerHTML),n.dataTransfer.effectAllowed="copyMove",$h||n.dataTransfer.setData("text/plain",l),t.dragging=new Dh(s,!n[Nh])},ah.dragend=t=>{let e=t.dragging;window.setTimeout((()=>{t.dragging==e&&(t.dragging=null)}),50)},lh.dragover=lh.dragenter=(t,e)=>e.preventDefault(),lh.drop=(t,e)=>{let n=e,r=t.dragging;if(t.dragging=null,!n.dataTransfer)return;let i=t.posAtCoords(ph(n));if(!i)return;let o=t.state.doc.resolve(i.pos);if(!o)return;let s=r&&r.slice;s?t.someProp("transformPasted",(t=>{s=t(s)})):s=Zf(t,n.dataTransfer.getData($h?"Text":"text/plain"),$h?null:n.dataTransfer.getData("text/html"),!1,o);let a=!(!r||n[Nh]);if(t.someProp("handleDrop",(e=>e(t,n,s||ac.empty,a))))return void n.preventDefault();if(!s)return;n.preventDefault();let l=s?function(t,e,n){let r=t.resolve(e);if(!n.content.size)return e;let i=n.content;for(let o=0;o=0;t--){let e=t==r.depth?0:r.pos<=(r.start(t+1)+r.end(t+1))/2?-1:1,n=r.index(t)+(e>0?1:0),s=r.node(t),a=!1;if(1==o)a=s.canReplace(n,n,i);else{let t=s.contentMatchAt(n).findWrapping(i.firstChild.type);a=t&&s.canReplaceWith(n,n,t[0])}if(a)return 0==e?r.pos:e<0?r.before(t+1):r.after(t+1)}return null}(t.state.doc,o.pos,s):o.pos;null==l&&(l=o.pos);let c=t.state.tr;a&&c.deleteSelection();let u=c.mapping.map(l),d=0==s.openStart&&0==s.openEnd&&1==s.content.childCount,f=c.doc;if(d?c.replaceRangeWith(u,u,s.content.firstChild):c.replaceRange(u,u,s),c.doc.eq(f))return;let h=c.doc.resolve(u);if(d&&Wu.isSelectable(s.content.firstChild)&&h.nodeAfter&&h.nodeAfter.sameMarkup(s.content.firstChild))c.setSelection(new Wu(h));else{let e=c.mapping.map(l);c.mapping.maps[c.mapping.maps.length-1].forEach(((t,n,r,i)=>e=i)),c.setSelection(Pf(t,h,c.doc.resolve(e)))}t.focus(),t.dispatch(c.setMeta("uiEvent","drop"))},ah.focus=t=>{t.focused||(t.domObserver.stop(),t.dom.classList.add("ProseMirror-focused"),t.domObserver.start(),t.focused=!0,setTimeout((()=>{t.docView&&t.hasFocus()&&!t.domObserver.currentSelection.eq(t.domSelection())&&Cf(t)}),20))},ah.blur=(t,e)=>{let n=e;t.focused&&(t.domObserver.stop(),t.dom.classList.remove("ProseMirror-focused"),t.domObserver.start(),n.relatedTarget&&t.dom.contains(n.relatedTarget)&&t.domObserver.currentSelection.clear(),t.focused=!1)},ah.beforeinput=(t,e)=>{if(gd&&xd&&"deleteContentBackward"==e.inputType){t.domObserver.flushSoon();let{domChangeCount:e}=t.input;setTimeout((()=>{if(t.input.domChangeCount!=e)return;if(t.dom.blur(),t.focus(),t.someProp("handleKeyDown",(e=>e(t,Pd(8,"Backspace")))))return;let{$cursor:n}=t.state.selection;n&&n.pos>0&&t.dispatch(t.state.tr.delete(n.pos-1,n.pos).scrollIntoView())}),50)}};for(let os in lh)ah[os]=lh[os];function Ah(t,e){if(t==e)return!0;for(let n in t)if(t[n]!==e[n])return!1;for(let n in e)if(!(n in t))return!1;return!0}class Eh{constructor(t,e){this.toDOM=t,this.spec=e||jh,this.side=this.spec.side||0}map(t,e,n,r){let{pos:i,deleted:o}=t.mapResult(e.from+r,this.side<0?-1:1);return o?null:new Rh(i-n,i-n,this)}valid(){return!0}eq(t){return this==t||t instanceof Eh&&(this.spec.key&&this.spec.key==t.spec.key||this.toDOM==t.toDOM&&Ah(this.spec,t.spec))}destroy(t){this.spec.destroy&&this.spec.destroy(t)}}class Ph{constructor(t,e){this.attrs=t,this.spec=e||jh}map(t,e,n,r){let i=t.map(e.from+r,this.spec.inclusiveStart?-1:1)-n,o=t.map(e.to+r,this.spec.inclusiveEnd?1:-1)-n;return i>=o?null:new Rh(i,o,this)}valid(t,e){return e.from=t&&(!i||i(s.spec))&&n.push(s.copy(s.from+r,s.to+r))}for(let o=0;ot){let s=this.children[o]+1;this.children[o+2].findInner(t-s,e-s,n,r+s,i)}}map(t,e,n){return this==Lh||0==t.maps.length?this:this.mapInner(t,e,0,0,n||jh)}mapInner(t,e,n,r,i){let o;for(let s=0;s{for(let s=0;sc+o)continue;let u=a[s]+o;e>=u?a[s+1]=t<=u?-2:-1:n>=i&&(l=r-n-(e-t))&&(a[s]+=l,a[s+1]+=l)}};for(let u=0;u=r.content.size){c=!0;continue}let d=n.map(t[u+1]+o,-1)-i,{index:f,offset:h}=r.content.findIndex(l),p=r.maybeChild(f);if(p&&h==l&&h+p.nodeSize==d){let r=a[u+2].mapInner(n,p,e+1,t[u]+o+1,s);r!=Lh?(a[u]=l,a[u+1]=d,a[u+2]=r):(a[u+1]=-2,c=!0)}else c=!0}if(c){let l=function(t,e,n,r,i,o,s){function a(t,e){for(let o=0;o{let s,a=o+n;if(s=Wh(e,t,a)){for(r||(r=this.children.slice());io&&e.to=t){this.children[s]==t&&(n=this.children[s+2]);break}let i=t+1,o=i+e.content.size;for(let s=0;si&&t.type instanceof Ph){let e=Math.max(i,t.from)-i,n=Math.min(o,t.to)-i;en.map(t,e,jh)));return Bh.from(n)}forChild(t,e){if(e.isLeaf)return Fh.empty;let n=[];for(let r=0;rn&&o.to{let a=Wh(t,e,s+n);if(a){o=!0;let t=Jh(a,e,n+s+1,r);t!=Lh&&i.push(s,s+e.nodeSize,t)}}));let s=Vh(o?qh(t):t,-n).sort(Kh);for(let a=0;a0;)e++;t.splice(e,0,n)}function Yh(t){let e=[];return t.someProp("decorations",(n=>{let r=n(t.state);r&&r!=Lh&&e.push(r)})),t.cursorWrapper&&e.push(Fh.create(t.state.doc,[t.cursorWrapper.deco])),Bh.from(e)}const Gh={childList:!0,characterData:!0,characterDataOldValue:!0,attributes:!0,attributeOldValue:!0,subtree:!0},Zh=fd&&hd<=11;class Xh{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}set(t){this.anchorNode=t.anchorNode,this.anchorOffset=t.anchorOffset,this.focusNode=t.focusNode,this.focusOffset=t.focusOffset}clear(){this.anchorNode=this.focusNode=null}eq(t){return t.anchorNode==this.anchorNode&&t.anchorOffset==this.anchorOffset&&t.focusNode==this.focusNode&&t.focusOffset==this.focusOffset}}class Qh{constructor(t,e){this.view=t,this.handleDOMChange=e,this.queue=[],this.flushingSoon=-1,this.observer=null,this.currentSelection=new Xh,this.onCharData=null,this.suppressingSelectionUpdates=!1,this.observer=window.MutationObserver&&new window.MutationObserver((t=>{for(let e=0;e"childList"==t.type&&t.removedNodes.length||"characterData"==t.type&&t.oldValue.length>t.target.nodeValue.length))?this.flushSoon():this.flush()})),Zh&&(this.onCharData=t=>{this.queue.push({target:t.target,type:"characterData",oldValue:t.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this)}flushSoon(){this.flushingSoon<0&&(this.flushingSoon=window.setTimeout((()=>{this.flushingSoon=-1,this.flush()}),20))}forceFlush(){this.flushingSoon>-1&&(window.clearTimeout(this.flushingSoon),this.flushingSoon=-1,this.flush())}start(){this.observer&&(this.observer.takeRecords(),this.observer.observe(this.view.dom,Gh)),this.onCharData&&this.view.dom.addEventListener("DOMCharacterDataModified",this.onCharData),this.connectSelection()}stop(){if(this.observer){let t=this.observer.takeRecords();if(t.length){for(let e=0;ethis.flush()),20)}this.observer.disconnect()}this.onCharData&&this.view.dom.removeEventListener("DOMCharacterDataModified",this.onCharData),this.disconnectSelection()}connectSelection(){this.view.dom.ownerDocument.addEventListener("selectionchange",this.onSelectionChange)}disconnectSelection(){this.view.dom.ownerDocument.removeEventListener("selectionchange",this.onSelectionChange)}suppressSelectionUpdates(){this.suppressingSelectionUpdates=!0,setTimeout((()=>this.suppressingSelectionUpdates=!1),50)}onSelectionChange(){if(If(this.view)){if(this.suppressingSelectionUpdates)return Cf(this.view);if(fd&&hd<=11&&!this.view.state.selection.empty){let t=this.view.domSelection();if(t.focusNode&&$d(t.focusNode,t.focusOffset,t.anchorNode,t.anchorOffset))return this.flushSoon()}this.flush()}}setCurSelection(){this.currentSelection.set(this.view.domSelection())}ignoreSelectionChange(t){if(0==t.rangeCount)return!0;let e=t.getRangeAt(0).commonAncestorContainer,n=this.view.docView.nearestDesc(e);return n&&n.ignoreMutation({type:"selection",target:3==e.nodeType?e.parentNode:e})?(this.setCurSelection(),!0):void 0}flush(){if(!this.view.docView||this.flushingSoon>-1)return;let t=this.observer?this.observer.takeRecords():[];this.queue.length&&(t=this.queue.concat(t),this.queue.length=0);let e=this.view.domSelection(),n=!this.suppressingSelectionUpdates&&!this.currentSelection.eq(e)&&If(this.view)&&!this.ignoreSelectionChange(e),r=-1,i=-1,o=!1,s=[];if(this.view.editable)for(let a=0;a1){let t=s.filter((t=>"BR"==t.nodeName));if(2==t.length){let e=t[0],n=t[1];e.parentNode&&e.parentNode.parentNode==n.parentNode?n.remove():e.remove()}}(r>-1||n)&&(r>-1&&(this.view.docView.markDirty(r,i),function(t){if(tp)return;tp=!0,"normal"==getComputedStyle(t.dom).whiteSpace&&console.warn("ProseMirror expects the CSS white-space property to be set, preferably to 'pre-wrap'. It is recommended to load style/prosemirror.css from the prosemirror-view package.")}(this.view)),this.handleDOMChange(r,i,o,s),this.view.docView&&this.view.docView.dirty?this.view.updateState(this.view.state):this.currentSelection.eq(e)||Cf(this.view),this.currentSelection.set(e))}registerMutation(t,e){if(e.indexOf(t.target)>-1)return null;let n=this.view.docView.nearestDesc(t.target);if("attributes"==t.type&&(n==this.view.docView||"contenteditable"==t.attributeName||"style"==t.attributeName&&!t.oldValue&&!t.target.getAttribute("style")))return null;if(!n||n.ignoreMutation(t))return null;if("childList"==t.type){for(let n=0;nDate.now()-50?t.input.lastSelectionOrigin:null,n=_f(t,e);if(n&&!t.state.selection.eq(n)){let r=t.state.tr.setSelection(n);"pointer"==e?r.setMeta("pointer",!0):"key"==e&&r.scrollIntoView(),t.dispatch(r)}return}let o=t.state.doc.resolve(e),s=o.sharedDepth(n);e=o.before(s+1),n=t.state.doc.resolve(n).after(s+1);let a=t.state.selection,l=function(t,e,n){let r,{node:i,fromOffset:o,toOffset:s,from:a,to:l}=t.docView.parseRange(e,n),c=t.domSelection(),u=c.anchorNode;if(u&&t.dom.contains(1==u.nodeType?u:u.parentNode)&&(r=[{node:u,offset:c.anchorOffset}],Ed(c)||r.push({node:c.focusNode,offset:c.focusOffset})),gd&&8===t.input.lastKeyCode)for(let g=s;g>o;g--){let t=i.childNodes[g-1],e=t.pmViewDesc;if("BR"==t.nodeName&&!e){s=g;break}if(!e||e.size)break}let d=t.state.doc,f=t.someProp("domParser")||Jc.fromSchema(t.state.schema),h=d.resolve(a),p=null,m=f.parse(i,{topNode:h.parent,topMatch:h.parent.contentMatchAt(h.index()),topOpen:!0,from:o,to:s,preserveWhitespace:"pre"!=h.parent.type.whitespace||"full",findPositions:r,ruleFromNode:ep,context:h});if(r&&null!=r[0].pos){let t=r[0].pos,e=r[1]&&r[1].pos;null==e&&(e=t),p={anchor:t+a,head:e+a}}return{doc:m,sel:p,from:a,to:l}}(t,e,n);if(gd&&t.cursorWrapper&&l.sel&&l.sel.anchor==t.cursorWrapper.deco.from&&l.sel.head==l.sel.anchor){let e=t.cursorWrapper.deco.type.toDOM.nextSibling,n=e&&e.nodeValue?e.nodeValue.length:1;l.sel={anchor:l.sel.anchor+n,head:l.sel.anchor+n}}let c,u,d=t.state.doc,f=d.slice(l.from,l.to);8===t.input.lastKeyCode&&Date.now()-100=s?o-r:0,a=o+(a-s),s=o}else if(a=a?o-r:0,s=o+(s-a),a=o}return{start:o,endA:s,endB:a}}(f.content,l.doc.content,l.from,c,u);if((bd&&t.input.lastIOSEnter>Date.now()-225||xd)&&i.some((t=>"DIV"==t.nodeName||"P"==t.nodeName))&&(!h||h.endA>=h.endB)&&t.someProp("handleKeyDown",(e=>e(t,Pd(13,"Enter")))))return void(t.input.lastIOSEnter=0);if(!h){if(!(r&&a instanceof Bu&&!a.empty&&a.$head.sameParent(a.$anchor))||t.composing||l.sel&&l.sel.anchor!=l.sel.head){if(l.sel){let e=rp(t,t.state.doc,l.sel);e&&!e.eq(t.state.selection)&&t.dispatch(t.state.tr.setSelection(e))}return}h={start:a.from,endA:a.to,endB:a.to}}t.input.domChangeCount++,t.state.selection.fromt.state.selection.from&&h.start<=t.state.selection.from+2&&t.state.selection.from>=l.from?h.start=t.state.selection.from:h.endA=t.state.selection.to-2&&t.state.selection.to<=l.to&&(h.endB+=t.state.selection.to-h.endA,h.endA=t.state.selection.to)),fd&&hd<=11&&h.endB==h.start+1&&h.endA==h.start&&h.start>l.from&&"  "==l.doc.textBetween(h.start-l.from-1,h.start-l.from+1)&&(h.start--,h.endA--,h.endB--);let p,m=l.doc.resolveNoCache(h.start-l.from),g=l.doc.resolveNoCache(h.endB-l.from),v=d.resolve(h.start),y=m.sameParent(g)&&m.parent.inlineContent&&v.end()>=h.endA;if((bd&&t.input.lastIOSEnter>Date.now()-225&&(!y||i.some((t=>"DIV"==t.nodeName||"P"==t.nodeName)))||!y&&m.pose(t,Pd(13,"Enter")))))return void(t.input.lastIOSEnter=0);if(t.state.selection.anchor>h.start&&function(t,e,n,r,i){if(!r.parent.isTextblock||n-e<=i.pos-r.pos||ip(r,!0,!1)n||ip(s,!0,!1)e(t,Pd(8,"Backspace")))))return void(xd&&gd&&t.domObserver.suppressSelectionUpdates());gd&&xd&&h.endB==h.start&&(t.input.lastAndroidDelete=Date.now()),xd&&!y&&m.start()!=g.start()&&0==g.parentOffset&&m.depth==g.depth&&l.sel&&l.sel.anchor==l.sel.head&&l.sel.head==h.endA&&(h.endB-=2,g=l.doc.resolveNoCache(h.endB-l.from),setTimeout((()=>{t.someProp("handleKeyDown",(function(e){return e(t,Pd(13,"Enter"))}))}),20));let b,w,x,S=h.start,k=h.endA;if(y)if(m.pos==g.pos)fd&&hd<=11&&0==m.parentOffset&&(t.domObserver.suppressSelectionUpdates(),setTimeout((()=>Cf(t)),20)),b=t.state.tr.delete(S,k),w=d.resolve(h.start).marksAcross(d.resolve(h.endA));else if(h.endA==h.endB&&(x=function(t,e){let n,r,i,o=t.firstChild.marks,s=e.firstChild.marks,a=o,l=s;for(let u=0;ut.mark(r.addToSet(t.marks));else{if(0!=a.length||1!=l.length)return null;r=l[0],n="remove",i=t=>t.mark(r.removeFromSet(t.marks))}let c=[];for(let u=0;un(t,S,k,e))))return;b=t.state.tr.insertText(e,S,k)}if(b||(b=t.state.tr.replace(S,k,l.doc.slice(h.start-l.from,h.endB-l.from))),l.sel){let e=rp(t,b.doc,l.sel);e&&!(gd&&xd&&t.composing&&e.empty&&(h.start!=h.endB||t.input.lastAndroidDeletee.content.size?null:Pf(t,e.resolve(n.anchor),e.resolve(n.head))}function ip(t,e,n){let r=t.depth,i=e?t.end():t.pos;for(;r>0&&(e||t.indexAfter(r)==t.node(r).childCount);)r--,i++,e=!1;if(n){let e=t.node(r).maybeChild(t.indexAfter(r));for(;e&&!e.isLeaf;)e=e.firstChild,i++}return i}class op{constructor(t,e){this._root=null,this.focused=!1,this.trackWrites=null,this.mounted=!1,this.markCursor=null,this.cursorWrapper=null,this.lastSelectedViewDesc=void 0,this.input=new ch,this.prevDirectPlugins=[],this.pluginViews=[],this.dragging=null,this._props=e,this.state=e.state,this.directPlugins=e.plugins||[],this.directPlugins.forEach(up),this.dispatch=this.dispatch.bind(this),this.dom=t&&t.mount||document.createElement("div"),t&&(t.appendChild?t.appendChild(this.dom):"function"==typeof t?t(this.dom):t.mount&&(this.mounted=!0)),this.editable=lp(this),ap(this),this.nodeViews=cp(this),this.docView=cf(this.state.doc,sp(this),Yh(this),this.dom,this),this.domObserver=new Qh(this,((t,e,n,r)=>np(this,t,e,n,r))),this.domObserver.start(),function(t){for(let e in ah){let n=ah[e];t.dom.addEventListener(e,t.input.eventHandlers[e]=e=>{!hh(t,e)||fh(t,e)||!t.editable&&e.type in lh||n(t,e)})}yd&&t.dom.addEventListener("input",(()=>null)),dh(t)}(this),this.updatePluginViews()}get composing(){return this.input.composing}get props(){if(this._props.state!=this.state){let t=this._props;this._props={};for(let e in t)this._props[e]=t[e];this._props.state=this.state}return this._props}update(t){t.handleDOMEvents!=this._props.handleDOMEvents&&dh(this),this._props=t,t.plugins&&(t.plugins.forEach(up),this.directPlugins=t.plugins),this.updateStateInner(t.state,!0)}setProps(t){let e={};for(let n in this._props)e[n]=this._props[n];e.state=this.state;for(let n in t)e[n]=t[n];this.update(e)}updateState(t){this.updateStateInner(t,this.state.plugins!=t.plugins)}updateStateInner(t,e){let n=this.state,r=!1,i=!1;if(t.storedMarks&&this.composing&&(Mh(this),i=!0),this.state=t,e){let t=cp(this);(function(t,e){let n=0,r=0;for(let i in t){if(t[i]!=e[i])return!0;n++}for(let i in e)r++;return n!=r})(t,this.nodeViews)&&(this.nodeViews=t,r=!0),dh(this)}this.editable=lp(this),ap(this);let o=Yh(this),s=sp(this),a=e?"reset":t.scrollToSelection>n.scrollToSelection?"to selection":"preserve",l=r||!this.docView.matchesNode(t.doc,s,o);!l&&t.selection.eq(n.selection)||(i=!0);let c="preserve"==a&&i&&null==this.dom.style.overflowAnchor&&function(t){let e,n,r=t.dom.getBoundingClientRect(),i=Math.max(0,r.top);for(let o=(r.left+r.right)/2,s=i+1;s=i-20){e=r,n=a.top;break}}return{refDOM:e,refTop:n,stack:Fd(t.dom)}}(this);if(i){this.domObserver.stop();let e=l&&(fd||gd)&&!this.composing&&!n.selection.empty&&!t.selection.empty&&function(t,e){let n=Math.min(t.$anchor.sharedDepth(t.head),e.$anchor.sharedDepth(e.head));return t.$anchor.start(n)!=e.$anchor.start(n)}(n.selection,t.selection);if(l){let n=gd?this.trackWrites=this.domSelection().focusNode:null;!r&&this.docView.update(t.doc,s,o,this)||(this.docView.updateOuterDeco([]),this.docView.destroy(),this.docView=cf(t.doc,s,o,this.dom,this)),n&&!this.trackWrites&&(e=!0)}e||!(this.input.mouseDown&&this.domObserver.currentSelection.eq(this.domSelection())&&function(t){let e=t.docView.domFromPos(t.state.selection.anchor,0),n=t.domSelection();return $d(e.node,e.offset,n.anchorNode,n.anchorOffset)}(this))?Cf(this,e):(Af(this,t.selection),this.domObserver.setCurSelection()),this.domObserver.start()}if(this.updatePluginViews(n),"reset"==a)this.dom.scrollTop=0;else if("to selection"==a){let e=this.domSelection().focusNode;if(this.someProp("handleScrollToSelection",(t=>t(this))));else if(t.selection instanceof Wu){let n=this.docView.domAfterPos(t.selection.from);1==n.nodeType&&jd(this,n.getBoundingClientRect(),e)}else jd(this,this.coordsAtPos(t.selection.head,1),e)}else c&&function({refDOM:t,refTop:e,stack:n}){let r=t?t.getBoundingClientRect().top:0;Ld(n,0==r?0:r-e)}(c)}destroyPluginViews(){let t;for(;t=this.pluginViews.pop();)t.destroy&&t.destroy()}updatePluginViews(t){if(t&&t.plugins==this.state.plugins&&this.directPlugins==this.prevDirectPlugins)for(let e=0;ee.ownerDocument.getSelection()),this._root=e;return t||document}posAtCoords(t){return Jd(this,t)}coordsAtPos(t,e=1){return Ud(this,t,e)}domAtPos(t,e=0){return this.docView.domFromPos(t,e)}nodeDOM(t){let e=this.docView.descAt(t);return e?e.nodeDOM:null}posAtDOM(t,e,n=-1){let r=this.docView.posFromDOM(t,e,n);if(null==r)throw new RangeError("DOM position not inside the editor");return r}endOfTextblock(t,e){return nf(this,e||this.state,t)}destroy(){this.docView&&(!function(t){t.domObserver.stop();for(let e in t.input.eventHandlers)t.dom.removeEventListener(e,t.input.eventHandlers[e]);clearTimeout(t.input.composingTimeout),clearTimeout(t.input.lastIOSEnterFallbackTimeout)}(this),this.destroyPluginViews(),this.mounted?(this.docView.update(this.state.doc,[],Yh(this),this),this.dom.textContent=""):this.dom.parentNode&&this.dom.parentNode.removeChild(this.dom),this.docView.destroy(),this.docView=null)}get isDestroyed(){return null==this.docView}dispatchEvent(t){return function(t,e){fh(t,e)||!ah[e.type]||!t.editable&&e.type in lh||ah[e.type](t,e)}(this,t)}dispatch(t){let e=this._props.dispatchTransaction;e?e.call(this,t):this.updateState(this.state.apply(t))}domSelection(){return this.root.getSelection()}}function sp(t){let e=Object.create(null);return e.class="ProseMirror",e.contenteditable=String(t.editable),e.translate="no",t.someProp("attributes",(n=>{if("function"==typeof n&&(n=n(t.state)),n)for(let t in n)"class"==t&&(e.class+=" "+n[t]),"style"==t?e.style=(e.style?e.style+";":"")+n[t]:e[t]||"contenteditable"==t||"nodeName"==t||(e[t]=String(n[t]))})),[Rh.node(0,t.state.doc.content.size,e)]}function ap(t){if(t.markCursor){let e=document.createElement("img");e.className="ProseMirror-separator",e.setAttribute("mark-placeholder","true"),e.setAttribute("alt",""),t.cursorWrapper={dom:e,deco:Rh.widget(t.state.selection.head,e,{raw:!0,marks:t.markCursor})}}else t.cursorWrapper=null}function lp(t){return!t.someProp("editable",(e=>!1===e(t.state)))}function cp(t){let e=Object.create(null);function n(t){for(let n in t)Object.prototype.hasOwnProperty.call(e,n)||(e[n]=t[n])}return t.someProp("nodeViews",n),t.someProp("markViews",n),e}function up(t){if(t.spec.state||t.spec.filterTransaction||t.spec.appendTransaction)throw new RangeError("Plugins passed directly to the view must not have a state component")}for(var dp={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'",229:"q"},fp={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"',229:"Q"},hp="undefined"!=typeof navigator&&/Chrome\/(\d+)/.exec(navigator.userAgent),pp="undefined"!=typeof navigator&&/Apple Computer/.test(navigator.vendor),mp="undefined"!=typeof navigator&&/Gecko\/\d+/.test(navigator.userAgent),gp="undefined"!=typeof navigator&&/Mac/.test(navigator.platform),vp="undefined"!=typeof navigator&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent),yp=hp&&(gp||+hp[1]<57)||mp&&gp,bp=0;bp<10;bp++)dp[48+bp]=dp[96+bp]=String(bp);for(bp=1;bp<=24;bp++)dp[bp+111]="F"+bp;for(bp=65;bp<=90;bp++)dp[bp]=String.fromCharCode(bp+32),fp[bp]=String.fromCharCode(bp);for(var wp in dp)fp.hasOwnProperty(wp)||(fp[wp]=dp[wp]);const xp="undefined"!=typeof navigator&&/Mac|iP(hone|[oa]d)/.test(navigator.platform);function Sp(t){let e,n,r,i,o=t.split(/-(?!$)/),s=o[o.length-1];"Space"==s&&(s=" ");for(let a=0;a127)&&(r=dp[n.keyCode])&&r!=i){let i=e[kp(r,n,!0)];if(i&&i(t.state,t.dispatch,t))return!0}else if(o&&n.shiftKey){let r=e[kp(i,n,!0)];if(r&&r(t.state,t.dispatch,t))return!0}return!1}}const Mp=(t,e)=>!t.selection.empty&&(e&&e(t.tr.deleteSelection().scrollIntoView()),!0);function Cp(t,e,n=!1){for(let r=t;r;r="start"==e?r.firstChild:r.lastChild){if(r.isTextblock)return!0;if(n&&1!=r.childCount)return!1}return!1}function $p(t){if(!t.parent.type.spec.isolating)for(let e=t.depth-1;e>=0;e--){if(t.index(e)>0)return t.doc.resolve(t.before(e+1));if(t.node(e).type.spec.isolating)break}return null}function Tp(t){if(!t.parent.type.spec.isolating)for(let e=t.depth-1;e>=0;e--){let n=t.node(e);if(t.index(e)+1{let{$head:n,$anchor:r}=t.selection;if(!n.parent.type.spec.code||!n.sameParent(r))return!1;let i=n.node(-1),o=n.indexAfter(-1),s=Dp(i.contentMatchAt(o));if(!s||!i.canReplaceWith(o,o,s))return!1;if(e){let r=n.after(),i=t.tr.replaceWith(r,r,s.createAndFill());i.setSelection(zu.near(i.doc.resolve(r),1)),e(i.scrollIntoView())}return!0};function Ap(t,e,n){let r,i,o=e.nodeBefore,s=e.nodeAfter;if(o.type.spec.isolating||s.type.spec.isolating)return!1;if(function(t,e,n){let r=e.nodeBefore,i=e.nodeAfter,o=e.index();return!(!(r&&i&&r.type.compatibleContent(i.type))||(!r.content.size&&e.parent.canReplace(o-1,o)?(n&&n(t.tr.delete(e.pos-r.nodeSize,e.pos).scrollIntoView()),0):!e.parent.canReplace(o,o+1)||!i.isTextblock&&!ku(t.doc,e.pos)||(n&&n(t.tr.clearIncompatible(e.pos,r.type,r.contentMatchAt(r.childCount)).join(e.pos).scrollIntoView()),0)))}(t,e,n))return!0;let a=e.parent.canReplace(e.index(),e.index()+1);if(a&&(r=(i=o.contentMatchAt(o.childCount)).findWrapping(s.type))&&i.matchType(r[0]||s.type).validEnd){if(n){let i=e.pos+s.nodeSize,a=ec.empty;for(let t=r.length-1;t>=0;t--)a=ec.from(r[t].create(null,a));a=ec.from(o.copy(a));let l=t.tr.step(new gu(e.pos-1,i,e.pos,i,new ac(a,1,0),r.length,!0)),c=i+2*r.length;ku(l.doc,c)&&l.join(c),n(l.scrollIntoView())}return!0}let l=zu.findFrom(e,1),c=l&&l.$from.blockRange(l.$to),u=c&&bu(c);if(null!=u&&u>=e.depth)return n&&n(t.tr.lift(c,u).scrollIntoView()),!0;if(a&&Cp(s,"start",!0)&&Cp(o,"end")){let r=o,i=[];for(;i.push(r),!r.isTextblock;)r=r.lastChild;let a=s,l=1;for(;!a.isTextblock;a=a.firstChild)l++;if(r.canReplace(r.childCount,r.childCount,a.content)){if(n){let r=ec.empty;for(let t=i.length-1;t>=0;t--)r=ec.from(i[t].copy(r));n(t.tr.step(new gu(e.pos-i.length,e.pos+s.nodeSize,e.pos+l,e.pos+s.nodeSize-l,new ac(r,i.length,0),0,!0)).scrollIntoView())}return!0}}return!1}function Ep(t){return function(e,n){let r=e.selection,i=t<0?r.$from:r.$to,o=i.depth;for(;i.node(o).isInline;){if(!o)return!1;o--}return!!i.node(o).isTextblock&&(n&&n(e.tr.setSelection(Bu.create(e.doc,t<0?i.start(o):i.end(o)))),!0)}}const Pp=Ep(-1),Ip=Ep(1);function Rp(t,e=null){return function(n,r){let{from:i,to:o}=n.selection,s=!1;return n.doc.nodesBetween(i,o,((r,i)=>{if(s)return!1;if(r.isTextblock&&!r.hasMarkup(t,e))if(r.type==t)s=!0;else{let e=n.doc.resolve(i),r=e.index();s=e.parent.canReplaceWith(r,r+1,t)}})),!!s&&(r&&r(n.tr.setBlockType(i,o,t,e).scrollIntoView()),!0)}}function zp(t,e=null){return function(n,r){let{empty:i,$cursor:o,ranges:s}=n.selection;if(i&&!o||!function(t,e,n){for(let r=0;r{if(s)return!1;s=t.inlineContent&&t.type.allowsMarkType(n)})),s)return!0}return!1}(n.doc,s,t))return!1;if(r)if(o)t.isInSet(n.storedMarks||o.marks())?r(n.tr.removeStoredMark(t)):r(n.tr.addStoredMark(t.create(e)));else{let i=!1,o=n.tr;for(let e=0;!i&&e{let{$cursor:r}=t.selection;if(!r||(n?!n.endOfTextblock("backward",t):r.parentOffset>0))return!1;let i=$p(r);if(!i){let n=r.blockRange(),i=n&&bu(n);return null!=i&&(e&&e(t.tr.lift(n,i).scrollIntoView()),!0)}let o=i.nodeBefore;if(!o.type.spec.isolating&&Ap(t,i,e))return!0;if(0==r.parent.content.size&&(Cp(o,"end")||Wu.isSelectable(o))){let n=Ou(t.doc,r.before(),r.after(),ac.empty);if(n&&n.slice.size{let{$head:r,empty:i}=t.selection,o=r;if(!i)return!1;if(r.parent.isTextblock){if(n?!n.endOfTextblock("backward",t):r.parentOffset>0)return!1;o=$p(r)}let s=o&&o.nodeBefore;return!(!s||!Wu.isSelectable(s))&&(e&&e(t.tr.setSelection(Wu.create(t.doc,o.pos-s.nodeSize)).scrollIntoView()),!0)})),Lp=jp(Mp,((t,e,n)=>{let{$cursor:r}=t.selection;if(!r||(n?!n.endOfTextblock("forward",t):r.parentOffset{let{$head:r,empty:i}=t.selection,o=r;if(!i)return!1;if(r.parent.isTextblock){if(n?!n.endOfTextblock("forward",t):r.parentOffset{let{$head:n,$anchor:r}=t.selection;return!(!n.parent.type.spec.code||!n.sameParent(r))&&(e&&e(t.tr.insertText("\n").scrollIntoView()),!0)}),((t,e)=>{let n=t.selection,{$from:r,$to:i}=n;if(n instanceof Ju||r.parent.inlineContent||i.parent.inlineContent)return!1;let o=Dp(i.parent.contentMatchAt(i.indexAfter()));if(!o||!o.isTextblock)return!1;if(e){let n=(!r.parentOffset&&i.index(){let{$cursor:n}=t.selection;if(!n||n.parent.content.size)return!1;if(n.depth>1&&n.after()!=n.end(-1)){let r=n.before();if(Su(t.doc,r))return e&&e(t.tr.split(r).scrollIntoView()),!0}let r=n.blockRange(),i=r&&bu(r);return null!=i&&(e&&e(t.tr.lift(r,i).scrollIntoView()),!0)}),((t,e)=>{let{$from:n,$to:r}=t.selection;if(t.selection instanceof Wu&&t.selection.node.isBlock)return!(!n.parentOffset||!Su(t.doc,n.pos))&&(e&&e(t.tr.split(n.pos).scrollIntoView()),!0);if(!n.parent.isBlock)return!1;if(e){let i=r.parentOffset==r.parent.content.size,o=t.tr;(t.selection instanceof Bu||t.selection instanceof Ju)&&o.deleteSelection();let s=0==n.depth?null:Dp(n.node(-1).contentMatchAt(n.indexAfter(-1))),a=i&&s?[{type:s}]:void 0,l=Su(o.doc,o.mapping.map(n.pos),1,a);if(a||l||!Su(o.doc,o.mapping.map(n.pos),1,s?[{type:s}]:void 0)||(s&&(a=[{type:s}]),l=!0),l&&(o.split(o.mapping.map(n.pos),1,a),!i&&!n.parentOffset&&n.parent.type!=s)){let t=o.mapping.map(n.before()),e=o.doc.resolve(t);s&&n.node(-1).canReplaceWith(e.index(),e.index()+1,s)&&o.setNodeMarkup(o.mapping.map(n.before()),s)}e(o.scrollIntoView())}return!0})),"Mod-Enter":Np,Backspace:Fp,"Mod-Backspace":Fp,"Shift-Backspace":Fp,Delete:Lp,"Mod-Delete":Lp,"Mod-a":(t,e)=>(e&&e(t.tr.setSelection(new Ju(t.doc))),!0)},Vp={"Ctrl-h":Bp.Backspace,"Alt-Backspace":Bp["Mod-Backspace"],"Ctrl-d":Bp.Delete,"Ctrl-Alt-Backspace":Bp["Mod-Delete"],"Alt-Delete":Bp["Mod-Delete"],"Alt-d":Bp["Mod-Delete"],"Ctrl-a":Pp,"Ctrl-e":Ip};for(let os in Bp)Vp[os]=Bp[os];const Wp=("undefined"!=typeof navigator?/Mac|iP(hone|[oa]d)/.test(navigator.platform):!("undefined"==typeof os||!os.platform)&&"darwin"==os.platform())?Vp:Bp;class qp{constructor(t,e){var n;this.match=t,this.match=t,this.handler="string"==typeof e?(n=e,function(t,e,r,i){let o=n;if(e[1]){let t=e[0].lastIndexOf(e[1]);o+=e[0].slice(t+e[1].length);let n=(r+=t)-i;n>0&&(o=e[0].slice(t-n,t)+o,r=i)}return t.tr.insertText(o,r,i)}):e}}function Jp({rules:t}){let e=new nd({state:{init:()=>null,apply(t,e){let n=t.getMeta(this);return n||(t.selectionSet||t.docChanged?null:e)}},props:{handleTextInput:(n,r,i,o)=>Kp(n,r,i,o,t,e),handleDOMEvents:{compositionend:n=>{setTimeout((()=>{let{$cursor:r}=n.state.selection;r&&Kp(n,r.pos,r.pos,"",t,e)}))}}},isInputRules:!0});return e}function Kp(t,e,n,r,i,o){if(t.composing)return!1;let s=t.state,a=s.doc.resolve(e);if(a.parent.type.spec.code)return!1;let l=a.parent.textBetween(Math.max(0,a.parentOffset-500),a.parentOffset,null,"")+r;for(let c=0;c{let n=t.plugins;for(let r=0;r=0;t--)n.step(r.steps[t].invert(r.docs[t]));if(i.text){let e=n.doc.resolve(i.from).marks();n.replaceWith(i.from,i.to,t.schema.text(i.text,e))}else n.delete(i.from,i.to);e(n)}return!0}}return!1};function Up(t,e,n=null,r){return new qp(t,((t,i,o,s)=>{let a=n instanceof Function?n(i):n,l=t.tr.delete(o,s),c=l.doc.resolve(o).blockRange(),u=c&&wu(c,e,a);if(!u)return null;l.wrap(c,u);let d=l.doc.resolve(o-1).nodeBefore;return d&&d.type==e&&ku(l.doc,o-1)&&(!r||r(i,d))&&l.join(o-1),l}))}function Yp(t,e,n=null){return new qp(t,((t,r,i,o)=>{let s=t.doc.resolve(i),a=n instanceof Function?n(r):n;return s.node(-1).canReplaceWith(s.index(-1),s.indexAfter(-1),e)?t.tr.delete(i,o).setBlockType(i,i,e,a):null}))}new qp(/--$/,"—"),new qp(/\.\.\.$/,"…"),new qp(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/,"“"),new qp(/"$/,"”"),new qp(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/,"‘"),new qp(/'$/,"’");const Gp=["ol",0],Zp=["ul",0],Xp=["li",0],Qp={attrs:{order:{default:1}},parseDOM:[{tag:"ol",getAttrs:t=>({order:t.hasAttribute("start")?+t.getAttribute("start"):1})}],toDOM:t=>1==t.attrs.order?Gp:["ol",{start:t.attrs.order},0]},tm={parseDOM:[{tag:"ul"}],toDOM:()=>Zp},em={parseDOM:[{tag:"li"}],toDOM:()=>Xp,defining:!0};function nm(t,e){let n={};for(let r in t)n[r]=t[r];for(let r in e)n[r]=e[r];return n}function rm(t,e,n){return t.append({ordered_list:nm(Qp,{content:"list_item+",group:n}),bullet_list:nm(tm,{content:"list_item+",group:n}),list_item:nm(em,{content:e})})}function im(t,e=null){return function(n,r){let{$from:i,$to:o}=n.selection,s=i.blockRange(o),a=!1,l=s;if(!s)return!1;if(s.depth>=2&&i.node(s.depth-1).type.compatibleContent(t)&&0==s.startIndex){if(0==i.index(s.depth-1))return!1;let t=n.doc.resolve(s.start-2);l=new kc(t,t,s.depth),s.endIndex=0;u--)o=ec.from(n[u].type.create(n[u].attrs,o));t.step(new gu(e.start-(r?2:0),e.end,e.start,e.end,new ac(o,0,0),n.length,!0));let s=0;for(let u=0;u=r.depth-3;t--)i=ec.from(r.node(t).copy(i));let s=r.indexAfter(-1){if(c>-1)return!1;t.isTextblock&&0==t.content.size&&(c=e+1)})),c>-1&&l.setSelection(zu.near(l.doc.resolve(c))),n(l.scrollIntoView())}return!0}let a=i.pos==r.end()?s.contentMatchAt(0).defaultType:null,l=e.tr.delete(r.pos,i.pos),c=a?[null,{type:a}]:void 0;return!!Su(l.doc,r.pos,2,c)&&(n&&n(l.split(r.pos,2,c).scrollIntoView()),!0)}}function sm(t){return function(e,n){let{$from:r,$to:i}=e.selection,o=r.blockRange(i,(e=>e.childCount>0&&e.firstChild.type==t));return!!o&&(!n||(r.node(o.depth-1).type==t?function(t,e,n,r){let i=t.tr,o=r.end,s=r.$to.end(r.depth);om;p--)h-=i.child(p).nodeSize,r.delete(h-1,h+1);let o=r.doc.resolve(n.start),s=o.nodeAfter;if(r.mapping.map(n.end)!=n.start+o.nodeAfter.nodeSize)return!1;let a=0==n.startIndex,l=n.endIndex==i.childCount,c=o.node(-1),u=o.index(-1);if(!c.canReplace(u+(a?0:1),u+1,s.content.append(l?ec.empty:ec.from(i))))return!1;let d=o.pos,f=d+s.nodeSize;return r.step(new gu(d-(a?1:0),f+(l?1:0),d+1,f-1,new ac((a?ec.empty:ec.from(i.copy(ec.empty))).append(l?ec.empty:ec.from(i.copy(ec.empty))),a?0:1,l?0:1),a?0:1)),e(r.scrollIntoView()),!0}(e,n,o)))}}function am(t){return function(e,n){let{$from:r,$to:i}=e.selection,o=r.blockRange(i,(e=>e.childCount>0&&e.firstChild.type==t));if(!o)return!1;let s=o.startIndex;if(0==s)return!1;let a=o.parent,l=a.child(s-1);if(l.type!=t)return!1;if(n){let r=l.lastChild&&l.lastChild.type==a.type,i=ec.from(r?t.create():null),s=new ac(ec.from(t.create(null,ec.from(a.type.create(null,i)))),r?3:1,0),c=o.start,u=o.end;n(e.tr.step(new gu(c-(r?3:1),u,c,u,s,1,!0)).scrollIntoView())}return!0}}var lm=function(){};lm.prototype.append=function(t){return t.length?(t=lm.from(t),!this.length&&t||t.length<200&&this.leafAppend(t)||this.length<200&&t.leafPrepend(this)||this.appendInner(t)):this},lm.prototype.prepend=function(t){return t.length?lm.from(t).append(this):this},lm.prototype.appendInner=function(t){return new um(this,t)},lm.prototype.slice=function(t,e){return void 0===t&&(t=0),void 0===e&&(e=this.length),t>=e?lm.empty:this.sliceInner(Math.max(0,t),Math.min(this.length,e))},lm.prototype.get=function(t){if(!(t<0||t>=this.length))return this.getInner(t)},lm.prototype.forEach=function(t,e,n){void 0===e&&(e=0),void 0===n&&(n=this.length),e<=n?this.forEachInner(t,e,n,0):this.forEachInvertedInner(t,e,n,0)},lm.prototype.map=function(t,e,n){void 0===e&&(e=0),void 0===n&&(n=this.length);var r=[];return this.forEach((function(e,n){return r.push(t(e,n))}),e,n),r},lm.from=function(t){return t instanceof lm?t:t&&t.length?new cm(t):lm.empty};var cm=function(t){function e(e){t.call(this),this.values=e}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={length:{configurable:!0},depth:{configurable:!0}};return e.prototype.flatten=function(){return this.values},e.prototype.sliceInner=function(t,n){return 0==t&&n==this.length?this:new e(this.values.slice(t,n))},e.prototype.getInner=function(t){return this.values[t]},e.prototype.forEachInner=function(t,e,n,r){for(var i=e;i=n;i--)if(!1===t(this.values[i],r+i))return!1},e.prototype.leafAppend=function(t){if(this.length+t.length<=200)return new e(this.values.concat(t.flatten()))},e.prototype.leafPrepend=function(t){if(this.length+t.length<=200)return new e(t.flatten().concat(this.values))},n.length.get=function(){return this.values.length},n.depth.get=function(){return 0},Object.defineProperties(e.prototype,n),e}(lm);lm.empty=new cm([]);var um=function(t){function e(e,n){t.call(this),this.left=e,this.right=n,this.length=e.length+n.length,this.depth=Math.max(e.depth,n.depth)+1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.flatten=function(){return this.left.flatten().concat(this.right.flatten())},e.prototype.getInner=function(t){return ti&&!1===this.right.forEachInner(t,Math.max(e-i,0),Math.min(this.length,n)-i,r+i))&&void 0)},e.prototype.forEachInvertedInner=function(t,e,n,r){var i=this.left.length;return!(e>i&&!1===this.right.forEachInvertedInner(t,e-i,Math.max(n,i)-i,r+i))&&(!(n=n?this.right.slice(t-n,e-n):this.left.slice(t,n).append(this.right.slice(0,e-n))},e.prototype.leafAppend=function(t){var n=this.right.leafAppend(t);if(n)return new e(this.left,n)},e.prototype.leafPrepend=function(t){var n=this.left.leafPrepend(t);if(n)return new e(n,this.right)},e.prototype.appendInner=function(t){return this.left.depth>=Math.max(this.right.depth,t.depth)+1?new e(this.left,new e(this.right,t)):new e(this,t)},e}(lm),dm=lm;class fm{constructor(t,e){this.items=t,this.eventCount=e}popEvent(t,e){if(0==this.eventCount)return null;let n,r,i=this.items.length;for(;;i--){if(this.items.get(i-1).selection){--i;break}}e&&(n=this.remapping(i,this.items.length),r=n.maps.length);let o,s,a=t.tr,l=[],c=[];return this.items.forEach(((t,e)=>{if(!t.step)return n||(n=this.remapping(i,e+1),r=n.maps.length),r--,void c.push(t);if(n){c.push(new hm(t.map));let e,i=t.step.map(n.slice(r));i&&a.maybeStep(i).doc&&(e=a.mapping.maps[a.mapping.maps.length-1],l.push(new hm(e,void 0,void 0,l.length+c.length))),r--,e&&n.appendMap(e,r)}else a.maybeStep(t.step);return t.selection?(o=n?t.selection.map(n.slice(r)):t.selection,s=new fm(this.items.slice(0,i).append(c.reverse().concat(l)),this.eventCount-1),!1):void 0}),this.items.length,0),{remaining:s,transform:a,selection:o}}addTransform(t,e,n,r){let i=[],o=this.eventCount,s=this.items,a=!r&&s.length?s.get(s.length-1):null;for(let c=0;cmm&&(s=function(t,e){let n;return t.forEach(((t,r)=>{if(t.selection&&0==e--)return n=r,!1})),t.slice(n)}(s,l),o-=l),new fm(s.append(i),o)}remapping(t,e){let n=new lu;return this.items.forEach(((e,r)=>{let i=null!=e.mirrorOffset&&r-e.mirrorOffset>=t?n.maps.length-e.mirrorOffset:void 0;n.appendMap(e.map,i)}),t,e),n}addMaps(t){return 0==this.eventCount?this:new fm(this.items.append(t.map((t=>new hm(t)))),this.eventCount)}rebased(t,e){if(!this.eventCount)return this;let n=[],r=Math.max(0,this.items.length-e),i=t.mapping,o=t.steps.length,s=this.eventCount;this.items.forEach((t=>{t.selection&&s--}),r);let a=e;this.items.forEach((e=>{let r=i.getMirror(--a);if(null==r)return;o=Math.min(o,r);let l=i.maps[r];if(e.step){let o=t.steps[r].invert(t.docs[r]),c=e.selection&&e.selection.map(i.slice(a+1,r));c&&s++,n.push(new hm(l,o,c))}else n.push(new hm(l))}),r);let l=[];for(let d=e;d500&&(u=u.compress(this.items.length-n.length)),u}emptyItemCount(){let t=0;return this.items.forEach((e=>{e.step||t++})),t}compress(t=this.items.length){let e=this.remapping(0,t),n=e.maps.length,r=[],i=0;return this.items.forEach(((o,s)=>{if(s>=t)r.push(o),o.selection&&i++;else if(o.step){let t=o.step.map(e.slice(n)),s=t&&t.getMap();if(n--,s&&e.appendMap(s,n),t){let a=o.selection&&o.selection.map(e.slice(n));a&&i++;let l,c=new hm(s.invert(),t,a),u=r.length-1;(l=r.length&&r[u].merge(c))?r[u]=l:r.push(c)}}else o.map&&n--}),this.items.length,0),new fm(dm.from(r.reverse()),i)}}fm.empty=new fm(dm.empty,0);class hm{constructor(t,e,n,r){this.map=t,this.step=e,this.selection=n,this.mirrorOffset=r}merge(t){if(this.step&&t.step&&!t.selection){let e=t.step.merge(this.step);if(e)return new hm(e.getMap().invert(),e,this.selection)}}}class pm{constructor(t,e,n,r){this.done=t,this.undone=e,this.prevRanges=n,this.prevTime=r}}const mm=20;function gm(t){let e=[];return t.forEach(((t,n,r,i)=>e.push(r,i))),e}function vm(t,e){if(!t)return null;let n=[];for(let r=0;rnew pm(fm.empty,fm.empty,null,0),apply:(e,n,r)=>function(t,e,n,r){let i,o=n.getMeta(Sm);if(o)return o.historyState;n.getMeta(km)&&(t=new pm(t.done,t.undone,null,0));let s=n.getMeta("appendedTransaction");if(0==n.steps.length)return t;if(s&&s.getMeta(Sm))return s.getMeta(Sm).redo?new pm(t.done.addTransform(n,void 0,r,xm(e)),t.undone,gm(n.mapping.maps[n.steps.length-1]),t.prevTime):new pm(t.done,t.undone.addTransform(n,void 0,r,xm(e)),null,t.prevTime);if(!1===n.getMeta("addToHistory")||s&&!1===s.getMeta("addToHistory"))return(i=n.getMeta("rebased"))?new pm(t.done.rebased(n,i),t.undone.rebased(n,i),vm(t.prevRanges,n.mapping),t.prevTime):new pm(t.done.addMaps(n.mapping.maps),t.undone.addMaps(n.mapping.maps),vm(t.prevRanges,n.mapping),t.prevTime);{let i=0==t.prevTime||!s&&(t.prevTime<(n.time||0)-r.newGroupDelay||!function(t,e){if(!e)return!1;if(!t.docChanged)return!0;let n=!1;return t.mapping.maps[0].forEach(((t,r)=>{for(let i=0;i=e[i]&&(n=!0)})),n}(n,t.prevRanges)),o=s?vm(t.prevRanges,n.mapping):gm(n.mapping.maps[n.steps.length-1]);return new pm(t.done.addTransform(n,i?e.selection.getBookmark():void 0,r,xm(e)),fm.empty,o,n.time)}}(n,r,e,t)},config:t,props:{handleDOMEvents:{beforeinput(t,e){let n=e.inputType,r="historyUndo"==n?_m:"historyRedo"==n?Mm:null;return!!r&&(e.preventDefault(),r(t.state,t.dispatch))}}}})}const _m=(t,e)=>{let n=Sm.getState(t);return!(!n||0==n.done.eventCount)&&(e&&ym(n,t,e,!1),!0)},Mm=(t,e)=>{let n=Sm.getState(t);return!(!n||0==n.undone.eventCount)&&(e&&ym(n,t,e,!0),!0)};function Cm(t){let e=Sm.getState(t);return e?e.done.eventCount:0}function $m(t){let e=Sm.getState(t);return e?e.undone.eventCount:0}var Tm={},Dm={},Nm={},Am={};Object.defineProperty(Am,"__esModule",{value:!0}),Am.default=void 0;var Em=Ll.withParams;Am.default=Em,function(t){Object.defineProperty(t,"__esModule",{value:!0}),t.req=t.regex=t.ref=t.len=void 0,Object.defineProperty(t,"withParams",{enumerable:!0,get:function(){return n.default}});var e,n=(e=Am)&&e.__esModule?e:{default:e};function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}var i=function(t){if(Array.isArray(t))return!!t.length;if(null==t)return!1;if(!1===t)return!0;if(t instanceof Date)return!isNaN(t.getTime());if("object"===r(t)){for(var e in t)return!0;return!1}return!!String(t).length};t.req=i;t.len=function(t){return Array.isArray(t)?t.length:"object"===r(t)?Object.keys(t).length:String(t).length};t.ref=function(t,e,n){return"function"==typeof t?t.call(e,n):n[t]};t.regex=function(t,e){return(0,n.default)({type:t},(function(t){return!i(t)||e.test(t)}))}}(Nm),Object.defineProperty(Dm,"__esModule",{value:!0}),Dm.default=void 0;var Pm=(0,Nm.regex)("alpha",/^[a-zA-Z]*$/);Dm.default=Pm;var Im={};Object.defineProperty(Im,"__esModule",{value:!0}),Im.default=void 0;var Rm=(0,Nm.regex)("alphaNum",/^[a-zA-Z0-9]*$/);Im.default=Rm;var zm={};Object.defineProperty(zm,"__esModule",{value:!0}),zm.default=void 0;var jm=(0,Nm.regex)("numeric",/^[0-9]*$/);zm.default=jm;var Fm={};Object.defineProperty(Fm,"__esModule",{value:!0}),Fm.default=void 0;var Lm=Nm;Fm.default=function(t,e){return(0,Lm.withParams)({type:"between",min:t,max:e},(function(n){return!(0,Lm.req)(n)||(!/\s/.test(n)||n instanceof Date)&&+t<=+n&&+e>=+n}))};var Bm={};Object.defineProperty(Bm,"__esModule",{value:!0}),Bm.default=void 0;var Vm=(0,Nm.regex)("email",/^(?:[A-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9]{2,}(?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])$/i);Bm.default=Vm;var Wm={};Object.defineProperty(Wm,"__esModule",{value:!0}),Wm.default=void 0;var qm=Nm,Jm=(0,qm.withParams)({type:"ipAddress"},(function(t){if(!(0,qm.req)(t))return!0;if("string"!=typeof t)return!1;var e=t.split(".");return 4===e.length&&e.every(Km)}));Wm.default=Jm;var Km=function(t){if(t.length>3||0===t.length)return!1;if("0"===t[0]&&"0"!==t)return!1;if(!t.match(/^\d+$/))return!1;var e=0|+t;return e>=0&&e<=255},Hm={};Object.defineProperty(Hm,"__esModule",{value:!0}),Hm.default=void 0;var Um=Nm;Hm.default=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:":";return(0,Um.withParams)({type:"macAddress"},(function(e){if(!(0,Um.req)(e))return!0;if("string"!=typeof e)return!1;var n="string"==typeof t&&""!==t?e.split(t):12===e.length||16===e.length?e.match(/.{2}/g):null;return null!==n&&(6===n.length||8===n.length)&&n.every(Ym)}))};var Ym=function(t){return t.toLowerCase().match(/^[0-9a-f]{2}$/)},Gm={};Object.defineProperty(Gm,"__esModule",{value:!0}),Gm.default=void 0;var Zm=Nm;Gm.default=function(t){return(0,Zm.withParams)({type:"maxLength",max:t},(function(e){return!(0,Zm.req)(e)||(0,Zm.len)(e)<=t}))};var Xm={};Object.defineProperty(Xm,"__esModule",{value:!0}),Xm.default=void 0;var Qm=Nm;Xm.default=function(t){return(0,Qm.withParams)({type:"minLength",min:t},(function(e){return!(0,Qm.req)(e)||(0,Qm.len)(e)>=t}))};var tg={};Object.defineProperty(tg,"__esModule",{value:!0}),tg.default=void 0;var eg=Nm,ng=(0,eg.withParams)({type:"required"},(function(t){return(0,eg.req)("string"==typeof t?t.trim():t)}));tg.default=ng;var rg={};Object.defineProperty(rg,"__esModule",{value:!0}),rg.default=void 0;var ig=Nm;rg.default=function(t){return(0,ig.withParams)({type:"requiredIf",prop:t},(function(e,n){return!(0,ig.ref)(t,this,n)||(0,ig.req)(e)}))};var og={};Object.defineProperty(og,"__esModule",{value:!0}),og.default=void 0;var sg=Nm;og.default=function(t){return(0,sg.withParams)({type:"requiredUnless",prop:t},(function(e,n){return!!(0,sg.ref)(t,this,n)||(0,sg.req)(e)}))};var ag={};Object.defineProperty(ag,"__esModule",{value:!0}),ag.default=void 0;var lg=Nm;ag.default=function(t){return(0,lg.withParams)({type:"sameAs",eq:t},(function(e,n){return e===(0,lg.ref)(t,this,n)}))};var cg={};Object.defineProperty(cg,"__esModule",{value:!0}),cg.default=void 0;var ug=(0,Nm.regex)("url",/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i);cg.default=ug;var dg={};Object.defineProperty(dg,"__esModule",{value:!0}),dg.default=void 0;var fg=Nm;dg.default=function(){for(var t=arguments.length,e=new Array(t),n=0;n0&&e.reduce((function(e,n){return e||n.apply(t,r)}),!1)}))};var hg={};Object.defineProperty(hg,"__esModule",{value:!0}),hg.default=void 0;var pg=Nm;hg.default=function(){for(var t=arguments.length,e=new Array(t),n=0;n0&&e.reduce((function(e,n){return e&&n.apply(t,r)}),!0)}))};var mg={};Object.defineProperty(mg,"__esModule",{value:!0}),mg.default=void 0;var gg=Nm;mg.default=function(t){return(0,gg.withParams)({type:"not"},(function(e,n){return!(0,gg.req)(e)||!t.call(this,e,n)}))};var vg={};Object.defineProperty(vg,"__esModule",{value:!0}),vg.default=void 0;var yg=Nm;vg.default=function(t){return(0,yg.withParams)({type:"minValue",min:t},(function(e){return!(0,yg.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e>=+t}))};var bg={};Object.defineProperty(bg,"__esModule",{value:!0}),bg.default=void 0;var wg=Nm;bg.default=function(t){return(0,wg.withParams)({type:"maxValue",max:t},(function(e){return!(0,wg.req)(e)||(!/\s/.test(e)||e instanceof Date)&&+e<=+t}))};var xg={};Object.defineProperty(xg,"__esModule",{value:!0}),xg.default=void 0;var Sg=(0,Nm.regex)("integer",/(^[0-9]*$)|(^-[0-9]+$)/);xg.default=Sg;var kg={};Object.defineProperty(kg,"__esModule",{value:!0}),kg.default=void 0;var Og=(0,Nm.regex)("decimal",/^[-]?\d*(\.\d+)?$/);kg.default=Og,function(t){function e(t){return(e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}Object.defineProperty(t,"__esModule",{value:!0}),Object.defineProperty(t,"alpha",{enumerable:!0,get:function(){return n.default}}),Object.defineProperty(t,"alphaNum",{enumerable:!0,get:function(){return r.default}}),Object.defineProperty(t,"and",{enumerable:!0,get:function(){return v.default}}),Object.defineProperty(t,"between",{enumerable:!0,get:function(){return o.default}}),Object.defineProperty(t,"decimal",{enumerable:!0,get:function(){return S.default}}),Object.defineProperty(t,"email",{enumerable:!0,get:function(){return s.default}}),t.helpers=void 0,Object.defineProperty(t,"integer",{enumerable:!0,get:function(){return x.default}}),Object.defineProperty(t,"ipAddress",{enumerable:!0,get:function(){return a.default}}),Object.defineProperty(t,"macAddress",{enumerable:!0,get:function(){return l.default}}),Object.defineProperty(t,"maxLength",{enumerable:!0,get:function(){return c.default}}),Object.defineProperty(t,"maxValue",{enumerable:!0,get:function(){return w.default}}),Object.defineProperty(t,"minLength",{enumerable:!0,get:function(){return u.default}}),Object.defineProperty(t,"minValue",{enumerable:!0,get:function(){return b.default}}),Object.defineProperty(t,"not",{enumerable:!0,get:function(){return y.default}}),Object.defineProperty(t,"numeric",{enumerable:!0,get:function(){return i.default}}),Object.defineProperty(t,"or",{enumerable:!0,get:function(){return g.default}}),Object.defineProperty(t,"required",{enumerable:!0,get:function(){return d.default}}),Object.defineProperty(t,"requiredIf",{enumerable:!0,get:function(){return f.default}}),Object.defineProperty(t,"requiredUnless",{enumerable:!0,get:function(){return h.default}}),Object.defineProperty(t,"sameAs",{enumerable:!0,get:function(){return p.default}}),Object.defineProperty(t,"url",{enumerable:!0,get:function(){return m.default}});var n=_(Dm),r=_(Im),i=_(zm),o=_(Fm),s=_(Bm),a=_(Wm),l=_(Hm),c=_(Gm),u=_(Xm),d=_(tg),f=_(rg),h=_(og),p=_(ag),m=_(cg),g=_(dg),v=_(hg),y=_(mg),b=_(vg),w=_(bg),x=_(xg),S=_(kg),k=function(t,n){if(!n&&t&&t.__esModule)return t;if(null===t||"object"!==e(t)&&"function"!=typeof t)return{default:t};var r=O(n);if(r&&r.has(t))return r.get(t);var i={},o=Object.defineProperty&&Object.getOwnPropertyDescriptor;for(var s in t)if("default"!==s&&Object.prototype.hasOwnProperty.call(t,s)){var a=o?Object.getOwnPropertyDescriptor(t,s):null;a&&(a.get||a.set)?Object.defineProperty(i,s,a):i[s]=t[s]}i.default=t,r&&r.set(t,i);return i}(Nm);function O(t){if("function"!=typeof WeakMap)return null;var e=new WeakMap,n=new WeakMap;return(O=function(t){return t?n:e})(t)}function _(t){return t&&t.__esModule?t:{default:t}}t.helpers=k}(Tm);export{$m as A,Om as B,Tm as C,Jc as D,td as E,ec as F,Tl as G,Zl as H,qp as I,Wu as N,nd as P,ac as S,Bu as T,Wn as V,ll as a,vl as b,Sl as c,wl as d,jp as e,Np as f,Up as g,Yp as h,rm as i,om as j,am as k,sm as l,cl as m,Op as n,Wc as o,Jp as p,op as q,eu as r,Rp as s,zp as t,Hp as u,Wp as v,im as w,_m as x,Mm as y,Cm as z}; diff --git a/kirby/panel/dist/js/vuedraggable.js b/kirby/panel/dist/js/vuedraggable.js new file mode 100644 index 0000000..2b85509 --- /dev/null +++ b/kirby/panel/dist/js/vuedraggable.js @@ -0,0 +1,7 @@ +/**! + * Sortable 1.10.2 + * @author RubaXa + * @author owenm + * @license MIT + */ +function t(e){return(t="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(e)}function e(t,e,n){return e in t?Object.defineProperty(t,e,{value:n,enumerable:!0,configurable:!0,writable:!0}):t[e]=n,t}function n(){return n=Object.assign||function(t){for(var e=1;e=0||(i[n]=t[n]);return i}(t,e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(t);for(o=0;o=0||Object.prototype.propertyIsEnumerable.call(t,n)&&(i[n]=t[n])}return i}function r(t){if("undefined"!=typeof window&&window.navigator)return!!navigator.userAgent.match(t)}var a=r(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),l=r(/Edge/i),s=r(/firefox/i),c=r(/safari/i)&&!r(/chrome/i)&&!r(/android/i),d=r(/iP(ad|od|hone)/i),u=r(/chrome/i)&&r(/android/i),h={capture:!1,passive:!1};function f(t,e,n){t.addEventListener(e,n,!a&&h)}function p(t,e,n){t.removeEventListener(e,n,!a&&h)}function g(t,e){if(e){if(">"===e[0]&&(e=e.substring(1)),t)try{if(t.matches)return t.matches(e);if(t.msMatchesSelector)return t.msMatchesSelector(e);if(t.webkitMatchesSelector)return t.webkitMatchesSelector(e)}catch(n){return!1}return!1}}function m(t){return t.host&&t!==document&&t.host.nodeType?t.host:t.parentNode}function v(t,e,n,o){if(t){n=n||document;do{if(null!=e&&(">"===e[0]?t.parentNode===n&&g(t,e):g(t,e))||o&&t===n)return t;if(t===n)break}while(t=m(t))}return null}var b,y=/\s+/g;function w(t,e,n){if(t&&e)if(t.classList)t.classList[n?"add":"remove"](e);else{var o=(" "+t.className+" ").replace(y," ").replace(" "+e+" "," ");t.className=(o+(n?" "+e:"")).replace(y," ")}}function E(t,e,n){var o=t&&t.style;if(o){if(void 0===n)return document.defaultView&&document.defaultView.getComputedStyle?n=document.defaultView.getComputedStyle(t,""):t.currentStyle&&(n=t.currentStyle),void 0===e?n:n[e];e in o||-1!==e.indexOf("webkit")||(e="-webkit-"+e),o[e]=n+("string"==typeof n?"":"px")}}function D(t,e){var n="";if("string"==typeof t)n=t;else do{var o=E(t,"transform");o&&"none"!==o&&(n=o+" "+n)}while(!e&&(t=t.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(n)}function _(t,e,n){if(t){var o=t.getElementsByTagName(e),i=0,r=o.length;if(n)for(;i=r:i<=r))return o;if(o===S())break;o=N(o,!1)}return!1}function T(t,e,n){for(var o=0,i=0,r=t.children;i2&&void 0!==arguments[2]?arguments[2]:{},r=n.evt,a=i(n,["evt"]);$.pluginEvent.bind(At)(t,e,o({dragEl:H,parentEl:W,ghostEl:V,rootEl:U,nextEl:z,lastDownEl:G,cloneEl:q,cloneHidden:J,dragStarted:ct,putSortable:nt,activeSortable:At.active,originalEvent:r,oldIndex:Z,oldDraggableIndex:Q,newIndex:K,newDraggableIndex:tt,hideGhostForTarget:It,unhideGhostForTarget:Mt,cloneNowHidden:function(){J=!0},cloneNowShown:function(){J=!1},dispatchSortableEvent:function(t){j({sortable:e,name:t,originalEvent:r})}},a))};function j(t){!function(t){var e=t.sortable,n=t.rootEl,i=t.name,r=t.targetEl,s=t.cloneEl,c=t.toEl,d=t.fromEl,u=t.oldIndex,h=t.newIndex,f=t.oldDraggableIndex,p=t.newDraggableIndex,g=t.originalEvent,m=t.putSortable,v=t.extraEventProperties;if(e=e||n&&n[R]){var b,y=e.options,w="on"+i.charAt(0).toUpperCase()+i.substr(1);!window.CustomEvent||a||l?(b=document.createEvent("Event")).initEvent(i,!0,!0):b=new CustomEvent(i,{bubbles:!0,cancelable:!0}),b.to=c||n,b.from=d||n,b.item=r||n,b.clone=s,b.oldIndex=u,b.newIndex=h,b.oldDraggableIndex=f,b.newDraggableIndex=p,b.originalEvent=g,b.pullMode=m?m.lastPutMode:void 0;var E=o({},v,$.getEventProperties(i,e));for(var D in E)b[D]=E[D];n&&n.dispatchEvent(b),y[w]&&y[w].call(e,b)}}(o({putSortable:nt,cloneEl:q,targetEl:H,rootEl:U,oldIndex:Z,oldDraggableIndex:Q,newIndex:K,newDraggableIndex:tt},t))}var H,W,V,U,z,G,q,J,Z,K,Q,tt,et,nt,ot,it,rt,at,lt,st,ct,dt,ut,ht,ft,pt=!1,gt=!1,mt=[],vt=!1,bt=!1,yt=[],wt=!1,Et=[],Dt="undefined"!=typeof document,_t=d,St=l||a?"cssFloat":"float",Ct=Dt&&!u&&!d&&"draggable"in document.createElement("div"),xt=function(){if(Dt){if(a)return!1;var t=document.createElement("x");return t.style.cssText="pointer-events:auto","auto"===t.style.pointerEvents}}(),Tt=function(t,e){var n=E(t),o=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),i=T(t,0,e),r=T(t,1,e),a=i&&E(i),l=r&&E(r),s=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+C(i).width,c=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+C(r).width;if("flex"===n.display)return"column"===n.flexDirection||"column-reverse"===n.flexDirection?"vertical":"horizontal";if("grid"===n.display)return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(i&&a.float&&"none"!==a.float){var d="left"===a.float?"left":"right";return!r||"both"!==l.clear&&l.clear!==d?"horizontal":"vertical"}return i&&("block"===a.display||"flex"===a.display||"table"===a.display||"grid"===a.display||s>=o&&"none"===n[St]||r&&"none"===n[St]&&s+c>o)?"vertical":"horizontal"},Ot=function(e){function n(t,e){return function(o,i,r,a){var l=o.options.group.name&&i.options.group.name&&o.options.group.name===i.options.group.name;if(null==t&&(e||l))return!0;if(null==t||!1===t)return!1;if(e&&"clone"===t)return t;if("function"==typeof t)return n(t(o,i,r,a),e)(o,i,r,a);var s=(e?o:i).options.group.name;return!0===t||"string"==typeof t&&t===s||t.join&&t.indexOf(s)>-1}}var o={},i=e.group;i&&"object"==t(i)||(i={name:i}),o.name=i.name,o.checkPull=n(i.pull,!0),o.checkPut=n(i.put),o.revertClone=i.revertClone,e.group=o},It=function(){!xt&&V&&E(V,"display","none")},Mt=function(){!xt&&V&&E(V,"display","")};Dt&&document.addEventListener("click",(function(t){if(gt)return t.preventDefault(),t.stopPropagation&&t.stopPropagation(),t.stopImmediatePropagation&&t.stopImmediatePropagation(),gt=!1,!1}),!0);var Nt=function(t){if(H){t=t.touches?t.touches[0]:t;var e=(i=t.clientX,r=t.clientY,mt.some((function(t){if(!O(t)){var e=C(t),n=t[R].options.emptyInsertThreshold,o=i>=e.left-n&&i<=e.right+n,l=r>=e.top-n&&r<=e.bottom+n;return n&&o&&l?a=t:void 0}})),a);if(e){var n={};for(var o in t)t.hasOwnProperty(o)&&(n[o]=t[o]);n.target=n.rootEl=e,n.preventDefault=void 0,n.stopPropagation=void 0,e[R]._onDragOver(n)}}var i,r,a},Pt=function(t){H&&H.parentNode[R]._isOutsideThisEl(t.target)};function At(t,e){if(!t||!t.nodeType||1!==t.nodeType)throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(t));this.el=t,this.options=e=n({},e),t[R]=this;var o={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(t.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Tt(t,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(t,e){t.setData("Text",e.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:!1!==At.supportPointer&&"PointerEvent"in window,emptyInsertThreshold:5};for(var i in $.initializePlugins(this,t,o),o)!(i in e)&&(e[i]=o[i]);for(var r in Ot(e),this)"_"===r.charAt(0)&&"function"==typeof this[r]&&(this[r]=this[r].bind(this));this.nativeDraggable=!e.forceFallback&&Ct,this.nativeDraggable&&(this.options.touchStartThreshold=1),e.supportPointer?f(t,"pointerdown",this._onTapStart):(f(t,"mousedown",this._onTapStart),f(t,"touchstart",this._onTapStart)),this.nativeDraggable&&(f(t,"dragover",this),f(t,"dragenter",this)),mt.push(this.el),e.store&&e.store.get&&this.sort(e.store.get(this)||[]),n(this,X())}function kt(t,e,n,o,i,r,s,c){var d,u,h=t[R],f=h.options.onMove;return!window.CustomEvent||a||l?(d=document.createEvent("Event")).initEvent("move",!0,!0):d=new CustomEvent("move",{bubbles:!0,cancelable:!0}),d.to=e,d.from=t,d.dragged=n,d.draggedRect=o,d.related=i||e,d.relatedRect=r||C(e),d.willInsertAfter=c,d.originalEvent=s,t.dispatchEvent(d),f&&(u=f.call(h,d,s)),u}function Ft(t){t.draggable=!1}function Rt(){wt=!1}function Xt(t){for(var e=t.tagName+t.className+t.src+t.href+t.textContent,n=e.length,o=0;n--;)o+=e.charCodeAt(n);return o.toString(36)}function Lt(t){return setTimeout(t,0)}function Yt(t){return clearTimeout(t)}At.prototype={constructor:At,_isOutsideThisEl:function(t){this.el.contains(t)||t===this.el||(dt=null)},_getDirection:function(t,e){return"function"==typeof this.options.direction?this.options.direction.call(this,t,e,H):this.options.direction},_onTapStart:function(t){if(t.cancelable){var e=this,n=this.el,o=this.options,i=o.preventOnFilter,r=t.type,a=t.touches&&t.touches[0]||t.pointerType&&"touch"===t.pointerType&&t,l=(a||t).target,s=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||l,c=o.filter;if(function(t){Et.length=0;var e=t.getElementsByTagName("input"),n=e.length;for(;n--;){var o=e[n];o.checked&&Et.push(o)}}(n),!H&&!(/mousedown|pointerdown/.test(r)&&0!==t.button||o.disabled||s.isContentEditable||(l=v(l,o.draggable,n,!1))&&l.animated||G===l)){if(Z=I(l),Q=I(l,o.draggable),"function"==typeof c){if(c.call(this,t,l,this))return j({sortable:e,rootEl:s,name:"filter",targetEl:l,toEl:n,fromEl:n}),B("filter",e,{evt:t}),void(i&&t.cancelable&&t.preventDefault())}else if(c&&(c=c.split(",").some((function(o){if(o=v(s,o.trim(),n,!1))return j({sortable:e,rootEl:o,name:"filter",targetEl:l,fromEl:n,toEl:n}),B("filter",e,{evt:t}),!0}))))return void(i&&t.cancelable&&t.preventDefault());o.handle&&!v(s,o.handle,n,!1)||this._prepareDragStart(t,a,l)}}},_prepareDragStart:function(t,e,n){var o,i=this,r=i.el,c=i.options,d=r.ownerDocument;if(n&&!H&&n.parentNode===r){var u=C(n);if(U=r,W=(H=n).parentNode,z=H.nextSibling,G=n,et=c.group,At.dragged=H,ot={target:H,clientX:(e||t).clientX,clientY:(e||t).clientY},lt=ot.clientX-u.left,st=ot.clientY-u.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,H.style["will-change"]="all",o=function(){B("delayEnded",i,{evt:t}),At.eventCanceled?i._onDrop():(i._disableDelayedDragEvents(),!s&&i.nativeDraggable&&(H.draggable=!0),i._triggerDragStart(t,e),j({sortable:i,name:"choose",originalEvent:t}),w(H,c.chosenClass,!0))},c.ignore.split(",").forEach((function(t){_(H,t.trim(),Ft)})),f(d,"dragover",Nt),f(d,"mousemove",Nt),f(d,"touchmove",Nt),f(d,"mouseup",i._onDrop),f(d,"touchend",i._onDrop),f(d,"touchcancel",i._onDrop),s&&this.nativeDraggable&&(this.options.touchStartThreshold=4,H.draggable=!0),B("delayStart",this,{evt:t}),!c.delay||c.delayOnTouchOnly&&!e||this.nativeDraggable&&(l||a))o();else{if(At.eventCanceled)return void this._onDrop();f(d,"mouseup",i._disableDelayedDrag),f(d,"touchend",i._disableDelayedDrag),f(d,"touchcancel",i._disableDelayedDrag),f(d,"mousemove",i._delayedDragTouchMoveHandler),f(d,"touchmove",i._delayedDragTouchMoveHandler),c.supportPointer&&f(d,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(o,c.delay)}}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){H&&Ft(H),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._disableDelayedDrag),p(t,"touchend",this._disableDelayedDrag),p(t,"touchcancel",this._disableDelayedDrag),p(t,"mousemove",this._delayedDragTouchMoveHandler),p(t,"touchmove",this._delayedDragTouchMoveHandler),p(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||"touch"==t.pointerType&&t,!this.nativeDraggable||e?this.options.supportPointer?f(document,"pointermove",this._onTouchMove):f(document,e?"touchmove":"mousemove",this._onTouchMove):(f(H,"dragend",this),f(U,"dragstart",this._onDragStart));try{document.selection?Lt((function(){document.selection.empty()})):window.getSelection().removeAllRanges()}catch(n){}},_dragStarted:function(t,e){if(pt=!1,U&&H){B("dragStarted",this,{evt:e}),this.nativeDraggable&&f(document,"dragover",Pt);var n=this.options;!t&&w(H,n.dragClass,!1),w(H,n.ghostClass,!0),At.active=this,t&&this._appendGhost(),j({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(it){this._lastX=it.clientX,this._lastY=it.clientY,It();for(var t=document.elementFromPoint(it.clientX,it.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(it.clientX,it.clientY))!==e;)e=t;if(H.parentNode[R]._isOutsideThisEl(t),e)do{if(e[R]){if(e[R]._onDragOver({clientX:it.clientX,clientY:it.clientY,target:t,rootEl:e})&&!this.options.dragoverBubble)break}t=e}while(e=e.parentNode);Mt()}},_onTouchMove:function(t){if(ot){var e=this.options,n=e.fallbackTolerance,o=e.fallbackOffset,i=t.touches?t.touches[0]:t,r=V&&D(V,!0),a=V&&r&&r.a,l=V&&r&&r.d,s=_t&&ft&&M(ft),c=(i.clientX-ot.clientX+o.x)/(a||1)+(s?s[0]-yt[0]:0)/(a||1),d=(i.clientY-ot.clientY+o.y)/(l||1)+(s?s[1]-yt[1]:0)/(l||1);if(!At.active&&!pt){if(n&&Math.max(Math.abs(i.clientX-this._lastX),Math.abs(i.clientY-this._lastY))o.right+i||t.clientX<=o.right&&t.clientY>o.bottom&&t.clientX>=o.left:t.clientX>o.right&&t.clientY>o.top||t.clientX<=o.right&&t.clientY>o.bottom+i}(t,r,this)&&!m.animated){if(m===H)return $(!1);if(m&&a===t.target&&(l=m),l&&(n=C(l)),!1!==kt(U,a,H,e,l,n,t,!!l))return Y(),a.appendChild(H),W=a,G(),$(!0)}else if(l.parentNode===a){n=C(l);var b,y,D,_=H.parentNode!==a,S=!function(t,e,n){var o=n?t.left:t.top,i=n?t.right:t.bottom,r=n?t.width:t.height,a=n?e.left:e.top,l=n?e.right:e.bottom,s=n?e.width:e.height;return o===a||i===l||o+r/2===a+s/2}(H.animated&&H.toRect||e,l.animated&&l.toRect||n,r),T=r?"top":"left",M=x(l,"top","top")||x(H,"top","top"),N=M?M.scrollTop:void 0;if(dt!==l&&(y=n[T],vt=!1,bt=!S&&s.invertSwap||_),b=function(t,e,n,o,i,r,a,l){var s=o?t.clientY:t.clientX,c=o?n.height:n.width,d=o?n.top:n.left,u=o?n.bottom:n.right,h=!1;if(!a)if(l&&htd+c*r/2:su-ht)return-ut}else if(s>d+c*(1-i)/2&&su-c*r/2))return s>d+c/2?1:-1;return 0}(t,l,n,r,S?1:s.swapThreshold,null==s.invertedSwapThreshold?s.swapThreshold:s.invertedSwapThreshold,bt,dt===l),0!==b){var P=I(H);do{P-=b,D=W.children[P]}while(D&&("none"===E(D,"display")||D===V))}if(0===b||D===l)return $(!1);dt=l,ut=b;var A=l.nextElementSibling,F=!1,X=kt(U,a,H,e,l,n,t,F=1===b);if(!1!==X)return 1!==X&&-1!==X||(F=1===X),wt=!0,setTimeout(Rt,30),Y(),F&&!A?a.appendChild(H):l.parentNode.insertBefore(H,F?A:l),M&&k(M,0,N-M.scrollTop),W=H.parentNode,void 0===y||bt||(ht=Math.abs(y-C(l)[T])),G(),$(!0)}if(a.contains(H))return $(!1)}return!1}function L(s,c){B(s,p,o({evt:t,isOwner:u,axis:r?"vertical":"horizontal",revert:i,dragRect:e,targetRect:n,canSort:h,fromSortable:f,target:l,completed:$,onMove:function(n,o){return kt(U,a,H,e,n,C(n),t,o)},changed:G},c))}function Y(){L("dragOverAnimationCapture"),p.captureAnimationState(),p!==f&&f.captureAnimationState()}function $(e){return L("dragOverCompleted",{insertion:e}),e&&(u?d._hideClone():d._showClone(p),p!==f&&(w(H,nt?nt.options.ghostClass:d.options.ghostClass,!1),w(H,s.ghostClass,!0)),nt!==p&&p!==At.active?nt=p:p===At.active&&nt&&(nt=null),f===p&&(p._ignoreWhileAnimating=l),p.animateAll((function(){L("dragOverAnimationComplete"),p._ignoreWhileAnimating=null})),p!==f&&(f.animateAll(),f._ignoreWhileAnimating=null)),(l===H&&!H.animated||l===a&&!l.animated)&&(dt=null),s.dragoverBubble||t.rootEl||l===document||(H.parentNode[R]._isOutsideThisEl(t.target),!e&&Nt(t)),!s.dragoverBubble&&t.stopPropagation&&t.stopPropagation(),g=!0}function G(){K=I(H),tt=I(H,s.draggable),j({sortable:p,name:"change",toEl:a,newIndex:K,newDraggableIndex:tt,originalEvent:t})}},_ignoreWhileAnimating:null,_offMoveEvents:function(){p(document,"mousemove",this._onTouchMove),p(document,"touchmove",this._onTouchMove),p(document,"pointermove",this._onTouchMove),p(document,"dragover",Nt),p(document,"mousemove",Nt),p(document,"touchmove",Nt)},_offUpEvents:function(){var t=this.el.ownerDocument;p(t,"mouseup",this._onDrop),p(t,"touchend",this._onDrop),p(t,"pointerup",this._onDrop),p(t,"touchcancel",this._onDrop),p(document,"selectstart",this)},_onDrop:function(t){var e=this.el,n=this.options;K=I(H),tt=I(H,n.draggable),B("drop",this,{evt:t}),W=H&&H.parentNode,K=I(H),tt=I(H,n.draggable),At.eventCanceled||(pt=!1,bt=!1,vt=!1,clearInterval(this._loopId),clearTimeout(this._dragStartTimer),Yt(this.cloneId),Yt(this._dragStartId),this.nativeDraggable&&(p(document,"drop",this),p(e,"dragstart",this._onDragStart)),this._offMoveEvents(),this._offUpEvents(),c&&E(document.body,"user-select",""),E(H,"transform",""),t&&(ct&&(t.cancelable&&t.preventDefault(),!n.dropBubble&&t.stopPropagation()),V&&V.parentNode&&V.parentNode.removeChild(V),(U===W||nt&&"clone"!==nt.lastPutMode)&&q&&q.parentNode&&q.parentNode.removeChild(q),H&&(this.nativeDraggable&&p(H,"dragend",this),Ft(H),H.style["will-change"]="",ct&&!pt&&w(H,nt?nt.options.ghostClass:this.options.ghostClass,!1),w(H,this.options.chosenClass,!1),j({sortable:this,name:"unchoose",toEl:W,newIndex:null,newDraggableIndex:null,originalEvent:t}),U!==W?(K>=0&&(j({rootEl:W,name:"add",toEl:W,fromEl:U,originalEvent:t}),j({sortable:this,name:"remove",toEl:W,originalEvent:t}),j({rootEl:W,name:"sort",toEl:W,fromEl:U,originalEvent:t}),j({sortable:this,name:"sort",toEl:W,originalEvent:t})),nt&&nt.save()):K!==Z&&K>=0&&(j({sortable:this,name:"update",toEl:W,originalEvent:t}),j({sortable:this,name:"sort",toEl:W,originalEvent:t})),At.active&&(null!=K&&-1!==K||(K=Z,tt=Q),j({sortable:this,name:"end",toEl:W,originalEvent:t}),this.save())))),this._nulling()},_nulling:function(){B("nulling",this),U=H=W=V=z=q=G=J=ot=it=ct=K=tt=Z=Q=dt=ut=nt=et=At.dragged=At.ghost=At.clone=At.active=null,Et.forEach((function(t){t.checked=!0})),Et.length=rt=at=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":H&&(this._onDragOver(t),function(t){t.dataTransfer&&(t.dataTransfer.dropEffect="move");t.cancelable&&t.preventDefault()}(t));break;case"selectstart":t.preventDefault()}},toArray:function(){for(var t,e=[],n=this.el.children,o=0,i=n.length,r=this.options;ot.replace(ee,((t,e)=>e?e.toUpperCase():""))));function oe(t){null!==t.parentElement&&t.parentElement.removeChild(t)}function ie(t,e,n){const o=0===n?t.children[0]:t.children[n-1].nextSibling;t.insertBefore(e,o)}function re(t,e){this.$nextTick((()=>this.$emit(t.toLowerCase(),e)))}function ae(t){return e=>{null!==this.realList&&this["onDrag"+t](e),re.call(this,t,e)}}function le(t){return["transition-group","TransitionGroup"].includes(t)}function se(t,e,n){return t[n]||(e[n]?e[n]():void 0)}const ce=["Start","Add","Remove","Update","End"],de=["Choose","Unchoose","Sort","Filter","Clone"],ue=["Move",...ce,...de].map((t=>"on"+t));var he=null;const fe={name:"draggable",inheritAttrs:!1,props:{options:Object,list:{type:Array,required:!1,default:null},value:{type:Array,required:!1,default:null},noTransitionOnDrag:{type:Boolean,default:!1},clone:{type:Function,default:t=>t},element:{type:String,default:"div"},tag:{type:String,default:null},move:{type:Function,default:null},componentData:{type:Object,required:!1,default:null}},data:()=>({transitionMode:!1,noneFunctionalComponentMode:!1}),render(t){const e=this.$slots.default;this.transitionMode=function(t){if(!t||1!==t.length)return!1;const[{componentOptions:e}]=t;return!!e&&le(e.tag)}(e);const{children:n,headerOffset:o,footerOffset:i}=function(t,e,n){let o=0,i=0;const r=se(e,n,"header");r&&(o=r.length,t=t?[...r,...t]:[...r]);const a=se(e,n,"footer");return a&&(i=a.length,t=t?[...t,...a]:[...a]),{children:t,headerOffset:o,footerOffset:i}}(e,this.$slots,this.$scopedSlots);this.headerOffset=o,this.footerOffset=i;const r=function(t,e){let n=null;const o=(t,e)=>{n=function(t,e,n){return void 0===n||((t=t||{})[e]=n),t}(n,t,e)};if(o("attrs",Object.keys(t).filter((t=>"id"===t||t.startsWith("data-"))).reduce(((e,n)=>(e[n]=t[n],e)),{})),!e)return n;const{on:i,props:r,attrs:a}=e;return o("on",i),o("props",r),Object.assign(n.attrs,a),n}(this.$attrs,this.componentData);return t(this.getTag(),r,n)},created(){null!==this.list&&null!==this.value&&te.error("Value and list props are mutually exclusive! Please set one or another."),"div"!==this.element&&te.warn("Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"),void 0!==this.options&&te.warn("Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props")},mounted(){if(this.noneFunctionalComponentMode=this.getTag().toLowerCase()!==this.$el.nodeName.toLowerCase()&&!this.getIsFunctional(),this.noneFunctionalComponentMode&&this.transitionMode)throw new Error(`Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}`);const t={};ce.forEach((e=>{t["on"+e]=ae.call(this,e)})),de.forEach((e=>{t["on"+e]=re.bind(this,e)}));const e=Object.keys(this.$attrs).reduce(((t,e)=>(t[ne(e)]=this.$attrs[e],t)),{}),n=Object.assign({},this.options,e,t,{onMove:(t,e)=>this.onDragMove(t,e)});!("draggable"in n)&&(n.draggable=">*"),this._sortable=new At(this.rootContainer,n),this.computeIndexes()},beforeDestroy(){void 0!==this._sortable&&this._sortable.destroy()},computed:{rootContainer(){return this.transitionMode?this.$el.children[0]:this.$el},realList(){return this.list?this.list:this.value}},watch:{options:{handler(t){this.updateOptions(t)},deep:!0},$attrs:{handler(t){this.updateOptions(t)},deep:!0},realList(){this.computeIndexes()}},methods:{getIsFunctional(){const{fnOptions:t}=this._vnode;return t&&t.functional},getTag(){return this.tag||this.element},updateOptions(t){for(var e in t){const n=ne(e);-1===ue.indexOf(n)&&this._sortable.option(n,t[e])}},getChildrenNodes(){if(this.noneFunctionalComponentMode)return this.$children[0].$slots.default;const t=this.$slots.default;return this.transitionMode?t[0].child.$slots.default:t},computeIndexes(){this.$nextTick((()=>{this.visibleIndexes=function(t,e,n,o){if(!t)return[];const i=t.map((t=>t.elm)),r=e.length-o,a=[...e].map(((t,e)=>e>=r?i.length:i.indexOf(t)));return n?a.filter((t=>-1!==t)):a}(this.getChildrenNodes(),this.rootContainer.children,this.transitionMode,this.footerOffset)}))},getUnderlyingVm(t){const e=function(t,e){return t.map((t=>t.elm)).indexOf(e)}(this.getChildrenNodes()||[],t);if(-1===e)return null;return{index:e,element:this.realList[e]}},getUnderlyingPotencialDraggableComponent:({__vue__:t})=>t&&t.$options&&le(t.$options._componentTag)?t.$parent:!("realList"in t)&&1===t.$children.length&&"realList"in t.$children[0]?t.$children[0]:t,emitChanges(t){this.$nextTick((()=>{this.$emit("change",t)}))},alterList(t){if(this.list)return void t(this.list);const e=[...this.value];t(e),this.$emit("input",e)},spliceList(){this.alterList((t=>t.splice(...arguments)))},updatePosition(t,e){this.alterList((n=>n.splice(e,0,n.splice(t,1)[0])))},getRelatedContextFromMoveEvent({to:t,related:e}){const n=this.getUnderlyingPotencialDraggableComponent(t);if(!n)return{component:n};const o=n.realList,i={list:o,component:n};if(t!==e&&o&&n.getUnderlyingVm){const t=n.getUnderlyingVm(e);if(t)return Object.assign(t,i)}return i},getVmIndex(t){const e=this.visibleIndexes,n=e.length;return t>n-1?n:e[t]},getComponent(){return this.$slots.default[0].componentInstance},resetTransitionData(t){if(!this.noTransitionOnDrag||!this.transitionMode)return;this.getChildrenNodes()[t].data=null;const e=this.getComponent();e.children=[],e.kept=void 0},onDragStart(t){this.context=this.getUnderlyingVm(t.item),t.item._underlying_vm_=this.clone(this.context.element),he=t.item},onDragAdd(t){const e=t.item._underlying_vm_;if(void 0===e)return;oe(t.item);const n=this.getVmIndex(t.newIndex);this.spliceList(n,0,e),this.computeIndexes();const o={element:e,newIndex:n};this.emitChanges({added:o})},onDragRemove(t){if(ie(this.rootContainer,t.item,t.oldIndex),"clone"===t.pullMode)return void oe(t.clone);const e=this.context.index;this.spliceList(e,1);const n={element:this.context.element,oldIndex:e};this.resetTransitionData(e),this.emitChanges({removed:n})},onDragUpdate(t){oe(t.item),ie(t.from,t.item,t.oldIndex);const e=this.context.index,n=this.getVmIndex(t.newIndex);this.updatePosition(e,n);const o={element:this.context.element,oldIndex:e,newIndex:n};this.emitChanges({moved:o})},updateProperty(t,e){t.hasOwnProperty(e)&&(t[e]+=this.headerOffset)},computeFutureIndex(t,e){if(!t.element)return 0;const n=[...e.to.children].filter((t=>"none"!==t.style.display)),o=n.indexOf(e.related),i=t.component.getVmIndex(o);return-1!==n.indexOf(he)||!e.willInsertAfter?i:i+1},onDragMove(t,e){const n=this.move;if(!n||!this.realList)return!0;const o=this.getRelatedContextFromMoveEvent(t),i=this.context,r=this.computeFutureIndex(o,t);Object.assign(i,{futureIndex:r});return n(Object.assign({},t,{relatedContext:o,draggedContext:i}),e)},onDragEnd(){this.computeIndexes(),he=null}}};"undefined"!=typeof window&&"Vue"in window&&window.Vue.component("draggable",fe);export{fe as default}; diff --git a/kirby/panel/vite.config.js b/kirby/panel/vite.config.js deleted file mode 100644 index 3768347..0000000 --- a/kirby/panel/vite.config.js +++ /dev/null @@ -1,115 +0,0 @@ -/* eslint-env node */ -import fs from "fs"; -import path from "path"; -import { defineConfig } from "vite"; -import { createVuePlugin } from "vite-plugin-vue2"; -import postcssAutoprefixer from "autoprefixer"; -import postcssCsso from "postcss-csso"; -import postcssDirPseudoClass from "postcss-dir-pseudo-class"; -import postcssLogical from "postcss-logical"; -import pluginRewriteAll from "vite-plugin-rewrite-all"; - -let custom; -try { - custom = require("./vite.config.custom.js"); -} catch (err) { - custom = {}; -} - -export default defineConfig(({ command }) => { - // Tell Kirby that we are in dev mode - if (command === "serve") { - // Create the flag file on start - const runningPath = __dirname + "/.vite-running"; - fs.closeSync(fs.openSync(runningPath, "w")); - - // Delete the flag file on any kind of exit - for (const eventType of ["exit", "SIGINT", "uncaughtException"]) { - process.on(eventType, function (err) { - if (fs.existsSync(runningPath) === true) { - fs.unlinkSync(runningPath); - } - - if (eventType === "uncaughtException") { - console.error(err); - } - - process.exit(); - }); - } - } - - const proxy = { - target: process.env.VUE_APP_DEV_SERVER || "http://sandbox.test", - changeOrigin: true, - secure: false - }; - - return { - plugins: [createVuePlugin(), pluginRewriteAll()], - define: { - // Fix vuelidate error - "process.env.BUILD": JSON.stringify("production") - }, - build: { - minify: "terser", - cssCodeSplit: false, - rollupOptions: { - input: "./src/index.js", - output: { - entryFileNames: "js/[name].js", - chunkFileNames: "js/[name].js", - assetFileNames: "[ext]/[name].[ext]" - } - } - }, - optimizeDeps: { - entries: "src/**/*.{js,vue}", - exclude: [ - "vitest" - ] - }, - css: { - postcss: { - plugins: [ - postcssLogical(), - postcssDirPseudoClass(), - postcssCsso(), - postcssAutoprefixer() - ] - } - }, - resolve: { - alias: [ - { - find: "vue", - replacement: "vue/dist/vue.esm.js" - }, - { - find: "@", - replacement: path.resolve(__dirname, "src") - } - ] - }, - server: { - proxy: { - "/api": proxy, - "/env": proxy, - "/media": proxy - }, - ...custom - }, - test: { - environment: "jsdom", - include: ["**/*.test.js"], - coverage: { - all: true, - exclude: ["**/*.e2e.js", "**/*.test.js"], - extension: ["js", "vue"], - src: "src", - reporter: ["text", "lcov"] - }, - setupFiles: ["vitest.setup.js"] - } - }; -}); diff --git a/kirby/panel/vitest.setup.js b/kirby/panel/vitest.setup.js deleted file mode 100644 index e5ea5f0..0000000 --- a/kirby/panel/vitest.setup.js +++ /dev/null @@ -1,4 +0,0 @@ -import Vue from "vue"; - -Vue.config.productionTip = false; -Vue.config.devtools = false; diff --git a/kirby/router.php b/kirby/router.php index 456f24a..10386c7 100644 --- a/kirby/router.php +++ b/kirby/router.php @@ -1,12 +1,12 @@ data($method, ...$args); - } - - /** - * Creates a new API instance - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setProperties($props); - } - - /** - * Runs the authentication method - * if set - * - * @return mixed - */ - public function authenticate() - { - if ($auth = $this->authentication()) { - return $auth->call($this); - } - - return true; - } - - /** - * Returns the authentication callback - * - * @return \Closure|null - */ - public function authentication() - { - return $this->authentication; - } - - /** - * Execute an API call for the given path, - * request method and optional request data - * - * @param string|null $path - * @param string $method - * @param array $requestData - * @return mixed - * @throws \Kirby\Exception\NotFoundException - * @throws \Exception - */ - public function call(string $path = null, string $method = 'GET', array $requestData = []) - { - $path = rtrim($path ?? '', '/'); - - $this->setRequestMethod($method); - $this->setRequestData($requestData); - - $this->router = new Router($this->routes()); - $this->route = $this->router->find($path, $method); - $auth = $this->route->attributes()['auth'] ?? true; - - if ($auth !== false) { - $user = $this->authenticate(); - - // set PHP locales based on *user* language - // so that e.g. strftime() gets formatted correctly - if (is_a($user, 'Kirby\Cms\User') === true) { - $language = $user->language(); - - // get the locale from the translation - $translation = $user->kirby()->translation($language); - $locale = ($translation !== null) ? $translation->locale() : $language; - - // provide some variants as fallbacks to be - // compatible with as many systems as possible - $locales = [ - $locale . '.UTF-8', - $locale . '.UTF8', - $locale . '.ISO8859-1', - $locale, - $language, - setlocale(LC_ALL, 0) // fall back to the previously defined locale - ]; - - // set the locales that are relevant for string formatting - // *don't* set LC_CTYPE to avoid breaking other parts of the system - setlocale(LC_MONETARY, $locales); - setlocale(LC_NUMERIC, $locales); - setlocale(LC_TIME, $locales); - } - } - - // don't throw pagination errors if pagination - // page is out of bounds - $validate = Pagination::$validate; - Pagination::$validate = false; - - $output = $this->route->action()->call($this, ...$this->route->arguments()); - - // restore old pagination validation mode - Pagination::$validate = $validate; - - if ( - is_object($output) === true && - is_a($output, 'Kirby\\Http\\Response') !== true - ) { - return $this->resolve($output)->toResponse(); - } - - return $output; - } - - /** - * Setter and getter for an API collection - * - * @param string $name - * @param array|null $collection - * @return \Kirby\Api\Collection - * @throws \Kirby\Exception\NotFoundException If no collection for `$name` exists - * @throws \Exception - */ - public function collection(string $name, $collection = null) - { - if (isset($this->collections[$name]) === false) { - throw new NotFoundException(sprintf('The collection "%s" does not exist', $name)); - } - - return new Collection($this, $collection, $this->collections[$name]); - } - - /** - * Returns the collections definition - * - * @return array - */ - public function collections(): array - { - return $this->collections; - } - - /** - * Returns the injected data array - * or certain parts of it by key - * - * @param string|null $key - * @param mixed ...$args - * @return mixed - * - * @throws \Kirby\Exception\NotFoundException If no data for `$key` exists - */ - public function data($key = null, ...$args) - { - if ($key === null) { - return $this->data; - } - - if ($this->hasData($key) === false) { - throw new NotFoundException(sprintf('Api data for "%s" does not exist', $key)); - } - - // lazy-load data wrapped in Closures - if (is_a($this->data[$key], 'Closure') === true) { - return $this->data[$key]->call($this, ...$args); - } - - return $this->data[$key]; - } - - /** - * Returns the debugging flag - * - * @return bool - */ - public function debug(): bool - { - return $this->debug; - } - - /** - * Checks if injected data exists for the given key - * - * @param string $key - * @return bool - */ - public function hasData(string $key): bool - { - return isset($this->data[$key]) === true; - } - - /** - * Matches an object with an array item - * based on the `type` field - * - * @param array models or collections - * @param mixed $object - * - * @return string key of match - */ - protected function match(array $array, $object = null) - { - foreach ($array as $definition => $model) { - if (is_a($object, $model['type']) === true) { - return $definition; - } - } - } - - /** - * Returns an API model instance by name - * - * @param string|null $name - * @param mixed $object - * @return \Kirby\Api\Model - * - * @throws \Kirby\Exception\NotFoundException If no model for `$name` exists - */ - public function model(string $name = null, $object = null) - { - // Try to auto-match object with API models - if ($name === null) { - if ($model = $this->match($this->models, $object)) { - $name = $model; - } - } - - if (isset($this->models[$name]) === false) { - throw new NotFoundException(sprintf('The model "%s" does not exist', $name)); - } - - return new Model($this, $object, $this->models[$name]); - } - - /** - * Returns all model definitions - * - * @return array - */ - public function models(): array - { - return $this->models; - } - - /** - * Getter for request data - * Can either get all the data - * or certain parts of it. - * - * @param string|null $type - * @param string|null $key - * @param mixed $default - * @return mixed - */ - public function requestData(string $type = null, string $key = null, $default = null) - { - if ($type === null) { - return $this->requestData; - } - - if ($key === null) { - return $this->requestData[$type] ?? []; - } - - $data = array_change_key_case($this->requestData($type)); - $key = strtolower($key); - - return $data[$key] ?? $default; - } - - /** - * Returns the request body if available - * - * @param string|null $key - * @param mixed $default - * @return mixed - */ - public function requestBody(string $key = null, $default = null) - { - return $this->requestData('body', $key, $default); - } - - /** - * Returns the files from the request if available - * - * @param string|null $key - * @param mixed $default - * @return mixed - */ - public function requestFiles(string $key = null, $default = null) - { - return $this->requestData('files', $key, $default); - } - - /** - * Returns all headers from the request if available - * - * @param string|null $key - * @param mixed $default - * @return mixed - */ - public function requestHeaders(string $key = null, $default = null) - { - return $this->requestData('headers', $key, $default); - } - - /** - * Returns the request method - * - * @return string - */ - public function requestMethod(): string - { - return $this->requestMethod; - } - - /** - * Returns the request query if available - * - * @param string|null $key - * @param mixed $default - * @return mixed - */ - public function requestQuery(string $key = null, $default = null) - { - return $this->requestData('query', $key, $default); - } - - /** - * Turns a Kirby object into an - * API model or collection representation - * - * @param mixed $object - * @return \Kirby\Api\Model|\Kirby\Api\Collection - * - * @throws \Kirby\Exception\NotFoundException If `$object` cannot be resolved - */ - public function resolve($object) - { - if (is_a($object, 'Kirby\Api\Model') === true || is_a($object, 'Kirby\Api\Collection') === true) { - return $object; - } - - if ($model = $this->match($this->models, $object)) { - return $this->model($model, $object); - } - - if ($collection = $this->match($this->collections, $object)) { - return $this->collection($collection, $object); - } - - throw new NotFoundException(sprintf('The object "%s" cannot be resolved', get_class($object))); - } - - /** - * Returns all defined routes - * - * @return array - */ - public function routes(): array - { - return $this->routes; - } - - /** - * Setter for the authentication callback - * - * @param \Closure|null $authentication - * @return $this - */ - protected function setAuthentication(Closure $authentication = null) - { - $this->authentication = $authentication; - return $this; - } - - /** - * Setter for the collections definition - * - * @param array|null $collections - * @return $this - */ - protected function setCollections(array $collections = null) - { - if ($collections !== null) { - $this->collections = array_change_key_case($collections); - } - return $this; - } - - /** - * Setter for the injected data - * - * @param array|null $data - * @return $this - */ - protected function setData(array $data = null) - { - $this->data = $data ?? []; - return $this; - } - - /** - * Setter for the debug flag - * - * @param bool $debug - * @return $this - */ - protected function setDebug(bool $debug = false) - { - $this->debug = $debug; - return $this; - } - - /** - * Setter for the model definitions - * - * @param array|null $models - * @return $this - */ - protected function setModels(array $models = null) - { - if ($models !== null) { - $this->models = array_change_key_case($models); - } - - return $this; - } - - /** - * Setter for the request data - * - * @param array|null $requestData - * @return $this - */ - protected function setRequestData(array $requestData = null) - { - $defaults = [ - 'query' => [], - 'body' => [], - 'files' => [] - ]; - - $this->requestData = array_merge($defaults, (array)$requestData); - return $this; - } - - /** - * Setter for the request method - * - * @param string|null $requestMethod - * @return $this - */ - protected function setRequestMethod(string $requestMethod = null) - { - $this->requestMethod = $requestMethod ?? 'GET'; - return $this; - } - - /** - * Setter for the route definitions - * - * @param array|null $routes - * @return $this - */ - protected function setRoutes(array $routes = null) - { - $this->routes = $routes ?? []; - return $this; - } - - /** - * Renders the API call - * - * @param string $path - * @param string $method - * @param array $requestData - * @return mixed - */ - public function render(string $path, $method = 'GET', array $requestData = []) - { - try { - $result = $this->call($path, $method, $requestData); - } catch (Throwable $e) { - $result = $this->responseForException($e); - } - - if ($result === null) { - $result = $this->responseFor404(); - } elseif ($result === false) { - $result = $this->responseFor400(); - } elseif ($result === true) { - $result = $this->responseFor200(); - } - - if (is_array($result) === false) { - return $result; - } - - // pretty print json data - $pretty = (bool)($requestData['query']['pretty'] ?? false) === true; - - if (($result['status'] ?? 'ok') === 'error') { - $code = $result['code'] ?? 400; - - // sanitize the error code - if ($code < 400 || $code > 599) { - $code = 500; - } - - return Response::json($result, $code, $pretty); - } - - return Response::json($result, 200, $pretty); - } - - /** - * Returns a 200 - ok - * response array. - * - * @return array - */ - public function responseFor200(): array - { - return [ - 'status' => 'ok', - 'message' => 'ok', - 'code' => 200 - ]; - } - - /** - * Returns a 400 - bad request - * response array. - * - * @return array - */ - public function responseFor400(): array - { - return [ - 'status' => 'error', - 'message' => 'bad request', - 'code' => 400, - ]; - } - - /** - * Returns a 404 - not found - * response array. - * - * @return array - */ - public function responseFor404(): array - { - return [ - 'status' => 'error', - 'message' => 'not found', - 'code' => 404, - ]; - } - - /** - * Creates the response array for - * an exception. Kirby exceptions will - * have more information - * - * @param \Throwable $e - * @return array - */ - public function responseForException(Throwable $e): array - { - // prepare the result array for all exception types - $result = [ - 'status' => 'error', - 'message' => $e->getMessage(), - 'code' => empty($e->getCode()) === true ? 500 : $e->getCode(), - 'exception' => get_class($e), - 'key' => null, - 'file' => F::relativepath($e->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? null), - 'line' => $e->getLine(), - 'details' => [], - 'route' => $this->route ? $this->route->pattern() : null - ]; - - // extend the information for Kirby Exceptions - if (is_a($e, 'Kirby\Exception\Exception') === true) { - $result['key'] = $e->getKey(); - $result['details'] = $e->getDetails(); - $result['code'] = $e->getHttpCode(); - } - - // remove critical info from the result set if - // debug mode is switched off - if ($this->debug !== true) { - unset( - $result['file'], - $result['exception'], - $result['line'], - $result['route'] - ); - } - - return $result; - } - - /** - * Upload helper method - * - * move_uploaded_file() not working with unit test - * Added debug parameter for testing purposes as we did in the Email class - * - * @param \Closure $callback - * @param bool $single - * @param bool $debug - * @return array - * - * @throws \Exception If request has no files or there was an error with the upload - */ - public function upload(Closure $callback, $single = false, $debug = false): array - { - $trials = 0; - $uploads = []; - $errors = []; - $files = $this->requestFiles(); - - // get error messages from translation - $errorMessages = [ - UPLOAD_ERR_INI_SIZE => t('upload.error.iniSize'), - UPLOAD_ERR_FORM_SIZE => t('upload.error.formSize'), - UPLOAD_ERR_PARTIAL => t('upload.error.partial'), - UPLOAD_ERR_NO_FILE => t('upload.error.noFile'), - UPLOAD_ERR_NO_TMP_DIR => t('upload.error.tmpDir'), - UPLOAD_ERR_CANT_WRITE => t('upload.error.cantWrite'), - UPLOAD_ERR_EXTENSION => t('upload.error.extension') - ]; - - if (empty($files) === true) { - $postMaxSize = Str::toBytes(ini_get('post_max_size')); - $uploadMaxFileSize = Str::toBytes(ini_get('upload_max_filesize')); - - if ($postMaxSize < $uploadMaxFileSize) { - throw new Exception(t('upload.error.iniPostSize')); - } else { - throw new Exception(t('upload.error.noFiles')); - } - } - - foreach ($files as $upload) { - if (isset($upload['tmp_name']) === false && is_array($upload)) { - continue; - } - - $trials++; - - try { - if ($upload['error'] !== 0) { - $errorMessage = $errorMessages[$upload['error']] ?? t('upload.error.default'); - throw new Exception($errorMessage); - } - - // get the extension of the uploaded file - $extension = F::extension($upload['name']); - - // try to detect the correct mime and add the extension - // accordingly. This will avoid .tmp filenames - if (empty($extension) === true || in_array($extension, ['tmp', 'temp'])) { - $mime = F::mime($upload['tmp_name']); - $extension = F::mimeToExtension($mime); - $filename = F::name($upload['name']) . '.' . $extension; - } else { - $filename = basename($upload['name']); - } - - $source = dirname($upload['tmp_name']) . '/' . uniqid() . '.' . $filename; - - // move the file to a location including the extension, - // for better mime detection - if ($debug === false && move_uploaded_file($upload['tmp_name'], $source) === false) { - throw new Exception(t('upload.error.cantMove')); - } - - $data = $callback($source, $filename); - - if (is_object($data) === true) { - $data = $this->resolve($data)->toArray(); - } - - $uploads[$upload['name']] = $data; - } catch (Exception $e) { - $errors[$upload['name']] = $e->getMessage(); - } - - if ($single === true) { - break; - } - } - - // return a single upload response - if ($trials === 1) { - if (empty($errors) === false) { - return [ - 'status' => 'error', - 'message' => current($errors) - ]; - } - - return [ - 'status' => 'ok', - 'data' => current($uploads) - ]; - } - - if (empty($errors) === false) { - return [ - 'status' => 'error', - 'errors' => $errors - ]; - } - - return [ - 'status' => 'ok', - 'data' => $uploads - ]; - } + use Properties; + + /** + * Authentication callback + * + * @var \Closure + */ + protected $authentication; + + /** + * Debugging flag + * + * @var bool + */ + protected $debug = false; + + /** + * Collection definition + * + * @var array + */ + protected $collections = []; + + /** + * Injected data/dependencies + * + * @var array + */ + protected $data = []; + + /** + * Model definitions + * + * @var array + */ + protected $models = []; + + /** + * The current route + * + * @var \Kirby\Http\Route + */ + protected $route; + + /** + * The Router instance + * + * @var \Kirby\Http\Router + */ + protected $router; + + /** + * Route definition + * + * @var array + */ + protected $routes = []; + + /** + * Request data + * [query, body, files] + * + * @var array + */ + protected $requestData = []; + + /** + * The applied request method + * (GET, POST, PATCH, etc.) + * + * @var string + */ + protected $requestMethod; + + /** + * Magic accessor for any given data + * + * @param string $method + * @param array $args + * @return mixed + * @throws \Kirby\Exception\NotFoundException + */ + public function __call(string $method, array $args = []) + { + return $this->data($method, ...$args); + } + + /** + * Creates a new API instance + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } + + /** + * Runs the authentication method + * if set + * + * @return mixed + */ + public function authenticate() + { + if ($auth = $this->authentication()) { + return $auth->call($this); + } + + return true; + } + + /** + * Returns the authentication callback + * + * @return \Closure|null + */ + public function authentication() + { + return $this->authentication; + } + + /** + * Execute an API call for the given path, + * request method and optional request data + * + * @param string|null $path + * @param string $method + * @param array $requestData + * @return mixed + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function call(string $path = null, string $method = 'GET', array $requestData = []) + { + $path = rtrim($path ?? '', '/'); + + $this->setRequestMethod($method); + $this->setRequestData($requestData); + + $this->router = new Router($this->routes()); + $this->route = $this->router->find($path, $method); + $auth = $this->route->attributes()['auth'] ?? true; + + if ($auth !== false) { + $user = $this->authenticate(); + + // set PHP locales based on *user* language + // so that e.g. strftime() gets formatted correctly + if (is_a($user, 'Kirby\Cms\User') === true) { + $language = $user->language(); + + // get the locale from the translation + $translation = $user->kirby()->translation($language); + $locale = ($translation !== null) ? $translation->locale() : $language; + + // provide some variants as fallbacks to be + // compatible with as many systems as possible + $locales = [ + $locale . '.UTF-8', + $locale . '.UTF8', + $locale . '.ISO8859-1', + $locale, + $language, + setlocale(LC_ALL, 0) // fall back to the previously defined locale + ]; + + // set the locales that are relevant for string formatting + // *don't* set LC_CTYPE to avoid breaking other parts of the system + setlocale(LC_MONETARY, $locales); + setlocale(LC_NUMERIC, $locales); + setlocale(LC_TIME, $locales); + } + } + + // don't throw pagination errors if pagination + // page is out of bounds + $validate = Pagination::$validate; + Pagination::$validate = false; + + $output = $this->route->action()->call($this, ...$this->route->arguments()); + + // restore old pagination validation mode + Pagination::$validate = $validate; + + if ( + is_object($output) === true && + is_a($output, 'Kirby\\Http\\Response') !== true + ) { + return $this->resolve($output)->toResponse(); + } + + return $output; + } + + /** + * Setter and getter for an API collection + * + * @param string $name + * @param array|null $collection + * @return \Kirby\Api\Collection + * @throws \Kirby\Exception\NotFoundException If no collection for `$name` exists + * @throws \Exception + */ + public function collection(string $name, $collection = null) + { + if (isset($this->collections[$name]) === false) { + throw new NotFoundException(sprintf('The collection "%s" does not exist', $name)); + } + + return new Collection($this, $collection, $this->collections[$name]); + } + + /** + * Returns the collections definition + * + * @return array + */ + public function collections(): array + { + return $this->collections; + } + + /** + * Returns the injected data array + * or certain parts of it by key + * + * @param string|null $key + * @param mixed ...$args + * @return mixed + * + * @throws \Kirby\Exception\NotFoundException If no data for `$key` exists + */ + public function data($key = null, ...$args) + { + if ($key === null) { + return $this->data; + } + + if ($this->hasData($key) === false) { + throw new NotFoundException(sprintf('Api data for "%s" does not exist', $key)); + } + + // lazy-load data wrapped in Closures + if (is_a($this->data[$key], 'Closure') === true) { + return $this->data[$key]->call($this, ...$args); + } + + return $this->data[$key]; + } + + /** + * Returns the debugging flag + * + * @return bool + */ + public function debug(): bool + { + return $this->debug; + } + + /** + * Checks if injected data exists for the given key + * + * @param string $key + * @return bool + */ + public function hasData(string $key): bool + { + return isset($this->data[$key]) === true; + } + + /** + * Matches an object with an array item + * based on the `type` field + * + * @param array models or collections + * @param mixed $object + * + * @return string key of match + */ + protected function match(array $array, $object = null) + { + foreach ($array as $definition => $model) { + if (is_a($object, $model['type']) === true) { + return $definition; + } + } + } + + /** + * Returns an API model instance by name + * + * @param string|null $name + * @param mixed $object + * @return \Kirby\Api\Model + * + * @throws \Kirby\Exception\NotFoundException If no model for `$name` exists + */ + public function model(string $name = null, $object = null) + { + // Try to auto-match object with API models + if ($name === null) { + if ($model = $this->match($this->models, $object)) { + $name = $model; + } + } + + if (isset($this->models[$name]) === false) { + throw new NotFoundException(sprintf('The model "%s" does not exist', $name)); + } + + return new Model($this, $object, $this->models[$name]); + } + + /** + * Returns all model definitions + * + * @return array + */ + public function models(): array + { + return $this->models; + } + + /** + * Getter for request data + * Can either get all the data + * or certain parts of it. + * + * @param string|null $type + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestData(string $type = null, string $key = null, $default = null) + { + if ($type === null) { + return $this->requestData; + } + + if ($key === null) { + return $this->requestData[$type] ?? []; + } + + $data = array_change_key_case($this->requestData($type)); + $key = strtolower($key); + + return $data[$key] ?? $default; + } + + /** + * Returns the request body if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestBody(string $key = null, $default = null) + { + return $this->requestData('body', $key, $default); + } + + /** + * Returns the files from the request if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestFiles(string $key = null, $default = null) + { + return $this->requestData('files', $key, $default); + } + + /** + * Returns all headers from the request if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestHeaders(string $key = null, $default = null) + { + return $this->requestData('headers', $key, $default); + } + + /** + * Returns the request method + * + * @return string + */ + public function requestMethod(): string + { + return $this->requestMethod; + } + + /** + * Returns the request query if available + * + * @param string|null $key + * @param mixed $default + * @return mixed + */ + public function requestQuery(string $key = null, $default = null) + { + return $this->requestData('query', $key, $default); + } + + /** + * Turns a Kirby object into an + * API model or collection representation + * + * @param mixed $object + * @return \Kirby\Api\Model|\Kirby\Api\Collection + * + * @throws \Kirby\Exception\NotFoundException If `$object` cannot be resolved + */ + public function resolve($object) + { + if (is_a($object, 'Kirby\Api\Model') === true || is_a($object, 'Kirby\Api\Collection') === true) { + return $object; + } + + if ($model = $this->match($this->models, $object)) { + return $this->model($model, $object); + } + + if ($collection = $this->match($this->collections, $object)) { + return $this->collection($collection, $object); + } + + throw new NotFoundException(sprintf('The object "%s" cannot be resolved', get_class($object))); + } + + /** + * Returns all defined routes + * + * @return array + */ + public function routes(): array + { + return $this->routes; + } + + /** + * Setter for the authentication callback + * + * @param \Closure|null $authentication + * @return $this + */ + protected function setAuthentication(Closure $authentication = null) + { + $this->authentication = $authentication; + return $this; + } + + /** + * Setter for the collections definition + * + * @param array|null $collections + * @return $this + */ + protected function setCollections(array $collections = null) + { + if ($collections !== null) { + $this->collections = array_change_key_case($collections); + } + return $this; + } + + /** + * Setter for the injected data + * + * @param array|null $data + * @return $this + */ + protected function setData(array $data = null) + { + $this->data = $data ?? []; + return $this; + } + + /** + * Setter for the debug flag + * + * @param bool $debug + * @return $this + */ + protected function setDebug(bool $debug = false) + { + $this->debug = $debug; + return $this; + } + + /** + * Setter for the model definitions + * + * @param array|null $models + * @return $this + */ + protected function setModels(array $models = null) + { + if ($models !== null) { + $this->models = array_change_key_case($models); + } + + return $this; + } + + /** + * Setter for the request data + * + * @param array|null $requestData + * @return $this + */ + protected function setRequestData(array $requestData = null) + { + $defaults = [ + 'query' => [], + 'body' => [], + 'files' => [] + ]; + + $this->requestData = array_merge($defaults, (array)$requestData); + return $this; + } + + /** + * Setter for the request method + * + * @param string|null $requestMethod + * @return $this + */ + protected function setRequestMethod(string $requestMethod = null) + { + $this->requestMethod = $requestMethod ?? 'GET'; + return $this; + } + + /** + * Setter for the route definitions + * + * @param array|null $routes + * @return $this + */ + protected function setRoutes(array $routes = null) + { + $this->routes = $routes ?? []; + return $this; + } + + /** + * Renders the API call + * + * @param string $path + * @param string $method + * @param array $requestData + * @return mixed + */ + public function render(string $path, $method = 'GET', array $requestData = []) + { + try { + $result = $this->call($path, $method, $requestData); + } catch (Throwable $e) { + $result = $this->responseForException($e); + } + + if ($result === null) { + $result = $this->responseFor404(); + } elseif ($result === false) { + $result = $this->responseFor400(); + } elseif ($result === true) { + $result = $this->responseFor200(); + } + + if (is_array($result) === false) { + return $result; + } + + // pretty print json data + $pretty = (bool)($requestData['query']['pretty'] ?? false) === true; + + if (($result['status'] ?? 'ok') === 'error') { + $code = $result['code'] ?? 400; + + // sanitize the error code + if ($code < 400 || $code > 599) { + $code = 500; + } + + return Response::json($result, $code, $pretty); + } + + return Response::json($result, 200, $pretty); + } + + /** + * Returns a 200 - ok + * response array. + * + * @return array + */ + public function responseFor200(): array + { + return [ + 'status' => 'ok', + 'message' => 'ok', + 'code' => 200 + ]; + } + + /** + * Returns a 400 - bad request + * response array. + * + * @return array + */ + public function responseFor400(): array + { + return [ + 'status' => 'error', + 'message' => 'bad request', + 'code' => 400, + ]; + } + + /** + * Returns a 404 - not found + * response array. + * + * @return array + */ + public function responseFor404(): array + { + return [ + 'status' => 'error', + 'message' => 'not found', + 'code' => 404, + ]; + } + + /** + * Creates the response array for + * an exception. Kirby exceptions will + * have more information + * + * @param \Throwable $e + * @return array + */ + public function responseForException(Throwable $e): array + { + if (isset($this->kirby) === true) { + $docRoot = $this->kirby->environment()->get('DOCUMENT_ROOT'); + } else { + $docRoot = $_SERVER['DOCUMENT_ROOT'] ?? null; + } + + // prepare the result array for all exception types + $result = [ + 'status' => 'error', + 'message' => $e->getMessage(), + 'code' => empty($e->getCode()) === true ? 500 : $e->getCode(), + 'exception' => get_class($e), + 'key' => null, + 'file' => F::relativepath($e->getFile(), $docRoot), + 'line' => $e->getLine(), + 'details' => [], + 'route' => $this->route ? $this->route->pattern() : null + ]; + + // extend the information for Kirby Exceptions + if (is_a($e, 'Kirby\Exception\Exception') === true) { + $result['key'] = $e->getKey(); + $result['details'] = $e->getDetails(); + $result['code'] = $e->getHttpCode(); + } + + // remove critical info from the result set if + // debug mode is switched off + if ($this->debug !== true) { + unset( + $result['file'], + $result['exception'], + $result['line'], + $result['route'] + ); + } + + return $result; + } + + /** + * Upload helper method + * + * move_uploaded_file() not working with unit test + * Added debug parameter for testing purposes as we did in the Email class + * + * @param \Closure $callback + * @param bool $single + * @param bool $debug + * @return array + * + * @throws \Exception If request has no files or there was an error with the upload + */ + public function upload(Closure $callback, $single = false, $debug = false): array + { + $trials = 0; + $uploads = []; + $errors = []; + $files = $this->requestFiles(); + + // get error messages from translation + $errorMessages = [ + UPLOAD_ERR_INI_SIZE => I18n::translate('upload.error.iniSize'), + UPLOAD_ERR_FORM_SIZE => I18n::translate('upload.error.formSize'), + UPLOAD_ERR_PARTIAL => I18n::translate('upload.error.partial'), + UPLOAD_ERR_NO_FILE => I18n::translate('upload.error.noFile'), + UPLOAD_ERR_NO_TMP_DIR => I18n::translate('upload.error.tmpDir'), + UPLOAD_ERR_CANT_WRITE => I18n::translate('upload.error.cantWrite'), + UPLOAD_ERR_EXTENSION => I18n::translate('upload.error.extension') + ]; + + if (empty($files) === true) { + $postMaxSize = Str::toBytes(ini_get('post_max_size')); + $uploadMaxFileSize = Str::toBytes(ini_get('upload_max_filesize')); + + if ($postMaxSize < $uploadMaxFileSize) { + throw new Exception(I18n::translate('upload.error.iniPostSize')); + } else { + throw new Exception(I18n::translate('upload.error.noFiles')); + } + } + + foreach ($files as $upload) { + if (isset($upload['tmp_name']) === false && is_array($upload)) { + continue; + } + + $trials++; + + try { + if ($upload['error'] !== 0) { + $errorMessage = $errorMessages[$upload['error']] ?? I18n::translate('upload.error.default'); + throw new Exception($errorMessage); + } + + // get the extension of the uploaded file + $extension = F::extension($upload['name']); + + // try to detect the correct mime and add the extension + // accordingly. This will avoid .tmp filenames + if (empty($extension) === true || in_array($extension, ['tmp', 'temp'])) { + $mime = F::mime($upload['tmp_name']); + $extension = F::mimeToExtension($mime); + $filename = F::name($upload['name']) . '.' . $extension; + } else { + $filename = basename($upload['name']); + } + + $source = dirname($upload['tmp_name']) . '/' . uniqid() . '.' . $filename; + + // move the file to a location including the extension, + // for better mime detection + if ($debug === false && move_uploaded_file($upload['tmp_name'], $source) === false) { + throw new Exception(I18n::translate('upload.error.cantMove')); + } + + $data = $callback($source, $filename); + + if (is_object($data) === true) { + $data = $this->resolve($data)->toArray(); + } + + $uploads[$upload['name']] = $data; + } catch (Exception $e) { + $errors[$upload['name']] = $e->getMessage(); + } + + if ($single === true) { + break; + } + } + + // return a single upload response + if ($trials === 1) { + if (empty($errors) === false) { + return [ + 'status' => 'error', + 'message' => current($errors) + ]; + } + + return [ + 'status' => 'ok', + 'data' => current($uploads) + ]; + } + + if (empty($errors) === false) { + return [ + 'status' => 'error', + 'errors' => $errors + ]; + } + + return [ + 'status' => 'ok', + 'data' => $uploads + ]; + } } diff --git a/kirby/src/Api/Collection.php b/kirby/src/Api/Collection.php index 01eb49b..48ddca2 100644 --- a/kirby/src/Api/Collection.php +++ b/kirby/src/Api/Collection.php @@ -19,160 +19,160 @@ use Kirby\Toolkit\Str; */ class Collection { - /** - * @var \Kirby\Api\Api - */ - protected $api; + /** + * @var \Kirby\Api\Api + */ + protected $api; - /** - * @var mixed|null - */ - protected $data; + /** + * @var mixed|null + */ + protected $data; - /** - * @var mixed|null - */ - protected $model; + /** + * @var mixed|null + */ + protected $model; - /** - * @var mixed|null - */ - protected $select; + /** + * @var mixed|null + */ + protected $select; - /** - * @var mixed|null - */ - protected $view; + /** + * @var mixed|null + */ + protected $view; - /** - * Collection constructor - * - * @param \Kirby\Api\Api $api - * @param mixed|null $data - * @param array $schema - * @throws \Exception - */ - public function __construct(Api $api, $data, array $schema) - { - $this->api = $api; - $this->data = $data; - $this->model = $schema['model'] ?? null; - $this->view = $schema['view'] ?? null; + /** + * Collection constructor + * + * @param \Kirby\Api\Api $api + * @param mixed|null $data + * @param array $schema + * @throws \Exception + */ + public function __construct(Api $api, $data, array $schema) + { + $this->api = $api; + $this->data = $data; + $this->model = $schema['model'] ?? null; + $this->view = $schema['view'] ?? null; - if ($data === null) { - if (is_a($schema['default'] ?? null, 'Closure') === false) { - throw new Exception('Missing collection data'); - } + if ($data === null) { + if (is_a($schema['default'] ?? null, 'Closure') === false) { + throw new Exception('Missing collection data'); + } - $this->data = $schema['default']->call($this->api); - } + $this->data = $schema['default']->call($this->api); + } - if ( - isset($schema['type']) === true && - is_a($this->data, $schema['type']) === false - ) { - throw new Exception('Invalid collection type'); - } - } + if ( + isset($schema['type']) === true && + is_a($this->data, $schema['type']) === false + ) { + throw new Exception('Invalid collection type'); + } + } - /** - * @param string|array|null $keys - * @return $this - * @throws \Exception - */ - public function select($keys = null) - { - if ($keys === false) { - return $this; - } + /** + * @param string|array|null $keys + * @return $this + * @throws \Exception + */ + public function select($keys = null) + { + if ($keys === false) { + return $this; + } - if (is_string($keys)) { - $keys = Str::split($keys); - } + if (is_string($keys)) { + $keys = Str::split($keys); + } - if ($keys !== null && is_array($keys) === false) { - throw new Exception('Invalid select keys'); - } + if ($keys !== null && is_array($keys) === false) { + throw new Exception('Invalid select keys'); + } - $this->select = $keys; - return $this; - } + $this->select = $keys; + return $this; + } - /** - * @return array - * @throws \Kirby\Exception\NotFoundException - * @throws \Exception - */ - public function toArray(): array - { - $result = []; + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toArray(): array + { + $result = []; - foreach ($this->data as $item) { - $model = $this->api->model($this->model, $item); + foreach ($this->data as $item) { + $model = $this->api->model($this->model, $item); - if ($this->view !== null) { - $model = $model->view($this->view); - } + if ($this->view !== null) { + $model = $model->view($this->view); + } - if ($this->select !== null) { - $model = $model->select($this->select); - } + if ($this->select !== null) { + $model = $model->select($this->select); + } - $result[] = $model->toArray(); - } + $result[] = $model->toArray(); + } - return $result; - } + return $result; + } - /** - * @return array - * @throws \Kirby\Exception\NotFoundException - * @throws \Exception - */ - public function toResponse(): array - { - if ($query = $this->api->requestQuery('query')) { - $this->data = $this->data->query($query); - } + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toResponse(): array + { + if ($query = $this->api->requestQuery('query')) { + $this->data = $this->data->query($query); + } - if (!$this->data->pagination()) { - $this->data = $this->data->paginate([ - 'page' => $this->api->requestQuery('page', 1), - 'limit' => $this->api->requestQuery('limit', 100) - ]); - } + if (!$this->data->pagination()) { + $this->data = $this->data->paginate([ + 'page' => $this->api->requestQuery('page', 1), + 'limit' => $this->api->requestQuery('limit', 100) + ]); + } - $pagination = $this->data->pagination(); + $pagination = $this->data->pagination(); - if ($select = $this->api->requestQuery('select')) { - $this->select($select); - } + if ($select = $this->api->requestQuery('select')) { + $this->select($select); + } - if ($view = $this->api->requestQuery('view')) { - $this->view($view); - } + if ($view = $this->api->requestQuery('view')) { + $this->view($view); + } - return [ - 'code' => 200, - 'data' => $this->toArray(), - 'pagination' => [ - 'page' => $pagination->page(), - 'total' => $pagination->total(), - 'offset' => $pagination->offset(), - 'limit' => $pagination->limit(), - ], - 'status' => 'ok', - 'type' => 'collection' - ]; - } + return [ + 'code' => 200, + 'data' => $this->toArray(), + 'pagination' => [ + 'page' => $pagination->page(), + 'total' => $pagination->total(), + 'offset' => $pagination->offset(), + 'limit' => $pagination->limit(), + ], + 'status' => 'ok', + 'type' => 'collection' + ]; + } - /** - * @param string $view - * @return $this - */ - public function view(string $view) - { - $this->view = $view; - return $this; - } + /** + * @param string $view + * @return $this + */ + public function view(string $view) + { + $this->view = $view; + return $this; + } } diff --git a/kirby/src/Api/Model.php b/kirby/src/Api/Model.php index 02118af..f0189c0 100644 --- a/kirby/src/Api/Model.php +++ b/kirby/src/Api/Model.php @@ -21,228 +21,228 @@ use Kirby\Toolkit\Str; */ class Model { - /** - * @var \Kirby\Api\Api - */ - protected $api; + /** + * @var \Kirby\Api\Api + */ + protected $api; - /** - * @var mixed|null - */ - protected $data; + /** + * @var mixed|null + */ + protected $data; - /** - * @var array|mixed - */ - protected $fields; + /** + * @var array|mixed + */ + protected $fields; - /** - * @var mixed|null - */ - protected $select; + /** + * @var mixed|null + */ + protected $select; - /** - * @var array|mixed - */ - protected $views; + /** + * @var array|mixed + */ + protected $views; - /** - * Model constructor - * - * @param \Kirby\Api\Api $api - * @param mixed $data - * @param array $schema - * @throws \Exception - */ - public function __construct(Api $api, $data, array $schema) - { - $this->api = $api; - $this->data = $data; - $this->fields = $schema['fields'] ?? []; - $this->select = $schema['select'] ?? null; - $this->views = $schema['views'] ?? []; + /** + * Model constructor + * + * @param \Kirby\Api\Api $api + * @param mixed $data + * @param array $schema + * @throws \Exception + */ + public function __construct(Api $api, $data, array $schema) + { + $this->api = $api; + $this->data = $data; + $this->fields = $schema['fields'] ?? []; + $this->select = $schema['select'] ?? null; + $this->views = $schema['views'] ?? []; - if ($this->select === null && array_key_exists('default', $this->views)) { - $this->view('default'); - } + if ($this->select === null && array_key_exists('default', $this->views)) { + $this->view('default'); + } - if ($data === null) { - if (is_a($schema['default'] ?? null, 'Closure') === false) { - throw new Exception('Missing model data'); - } + if ($data === null) { + if (is_a($schema['default'] ?? null, 'Closure') === false) { + throw new Exception('Missing model data'); + } - $this->data = $schema['default']->call($this->api); - } + $this->data = $schema['default']->call($this->api); + } - if ( - isset($schema['type']) === true && - is_a($this->data, $schema['type']) === false - ) { - throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type'])); - } - } + if ( + isset($schema['type']) === true && + is_a($this->data, $schema['type']) === false + ) { + throw new Exception(sprintf('Invalid model type "%s" expected: "%s"', get_class($this->data), $schema['type'])); + } + } - /** - * @param null $keys - * @return $this - * @throws \Exception - */ - public function select($keys = null) - { - if ($keys === false) { - return $this; - } + /** + * @param null $keys + * @return $this + * @throws \Exception + */ + public function select($keys = null) + { + if ($keys === false) { + return $this; + } - if (is_string($keys)) { - $keys = Str::split($keys); - } + if (is_string($keys)) { + $keys = Str::split($keys); + } - if ($keys !== null && is_array($keys) === false) { - throw new Exception('Invalid select keys'); - } + if ($keys !== null && is_array($keys) === false) { + throw new Exception('Invalid select keys'); + } - $this->select = $keys; - return $this; - } + $this->select = $keys; + return $this; + } - /** - * @return array - * @throws \Exception - */ - public function selection(): array - { - $select = $this->select; + /** + * @return array + * @throws \Exception + */ + public function selection(): array + { + $select = $this->select; - if ($select === null) { - $select = array_keys($this->fields); - } + if ($select === null) { + $select = array_keys($this->fields); + } - $selection = []; + $selection = []; - foreach ($select as $key => $value) { - if (is_int($key) === true) { - $selection[$value] = [ - 'view' => null, - 'select' => null - ]; - continue; - } + foreach ($select as $key => $value) { + if (is_int($key) === true) { + $selection[$value] = [ + 'view' => null, + 'select' => null + ]; + continue; + } - if (is_string($value) === true) { - if ($value === 'any') { - throw new Exception('Invalid sub view: "any"'); - } + if (is_string($value) === true) { + if ($value === 'any') { + throw new Exception('Invalid sub view: "any"'); + } - $selection[$key] = [ - 'view' => $value, - 'select' => null - ]; + $selection[$key] = [ + 'view' => $value, + 'select' => null + ]; - continue; - } + continue; + } - if (is_array($value) === true) { - $selection[$key] = [ - 'view' => null, - 'select' => $value - ]; - } - } + if (is_array($value) === true) { + $selection[$key] = [ + 'view' => null, + 'select' => $value + ]; + } + } - return $selection; - } + return $selection; + } - /** - * @return array - * @throws \Kirby\Exception\NotFoundException - * @throws \Exception - */ - public function toArray(): array - { - $select = $this->selection(); - $result = []; + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toArray(): array + { + $select = $this->selection(); + $result = []; - foreach ($this->fields as $key => $resolver) { - if (array_key_exists($key, $select) === false || is_a($resolver, 'Closure') === false) { - continue; - } + foreach ($this->fields as $key => $resolver) { + if (array_key_exists($key, $select) === false || is_a($resolver, 'Closure') === false) { + continue; + } - $value = $resolver->call($this->api, $this->data); + $value = $resolver->call($this->api, $this->data); - if (is_object($value)) { - $value = $this->api->resolve($value); - } + if (is_object($value)) { + $value = $this->api->resolve($value); + } - if ( - is_a($value, 'Kirby\Api\Collection') === true || - is_a($value, 'Kirby\Api\Model') === true - ) { - $selection = $select[$key]; + if ( + is_a($value, 'Kirby\Api\Collection') === true || + is_a($value, 'Kirby\Api\Model') === true + ) { + $selection = $select[$key]; - if ($subview = $selection['view']) { - $value->view($subview); - } + if ($subview = $selection['view']) { + $value->view($subview); + } - if ($subselect = $selection['select']) { - $value->select($subselect); - } + if ($subselect = $selection['select']) { + $value->select($subselect); + } - $value = $value->toArray(); - } + $value = $value->toArray(); + } - $result[$key] = $value; - } + $result[$key] = $value; + } - ksort($result); + ksort($result); - return $result; - } + return $result; + } - /** - * @return array - * @throws \Kirby\Exception\NotFoundException - * @throws \Exception - */ - public function toResponse(): array - { - $model = $this; + /** + * @return array + * @throws \Kirby\Exception\NotFoundException + * @throws \Exception + */ + public function toResponse(): array + { + $model = $this; - if ($select = $this->api->requestQuery('select')) { - $model = $model->select($select); - } + if ($select = $this->api->requestQuery('select')) { + $model = $model->select($select); + } - if ($view = $this->api->requestQuery('view')) { - $model = $model->view($view); - } + if ($view = $this->api->requestQuery('view')) { + $model = $model->view($view); + } - return [ - 'code' => 200, - 'data' => $model->toArray(), - 'status' => 'ok', - 'type' => 'model' - ]; - } + return [ + 'code' => 200, + 'data' => $model->toArray(), + 'status' => 'ok', + 'type' => 'model' + ]; + } - /** - * @param string $name - * @return $this - * @throws \Exception - */ - public function view(string $name) - { - if ($name === 'any') { - return $this->select(null); - } + /** + * @param string $name + * @return $this + * @throws \Exception + */ + public function view(string $name) + { + if ($name === 'any') { + return $this->select(null); + } - if (isset($this->views[$name]) === false) { - $name = 'default'; + if (isset($this->views[$name]) === false) { + $name = 'default'; - // try to fall back to the default view at least - if (isset($this->views[$name]) === false) { - throw new Exception(sprintf('The view "%s" does not exist', $name)); - } - } + // try to fall back to the default view at least + if (isset($this->views[$name]) === false) { + throw new Exception(sprintf('The view "%s" does not exist', $name)); + } + } - return $this->select($this->views[$name]); - } + return $this->select($this->views[$name]); + } } diff --git a/kirby/src/Cache/ApcuCache.php b/kirby/src/Cache/ApcuCache.php index ba0b4f9..fb11eda 100644 --- a/kirby/src/Cache/ApcuCache.php +++ b/kirby/src/Cache/ApcuCache.php @@ -15,72 +15,72 @@ use APCUIterator; */ class ApcuCache extends Cache { - /** - * Determines if an item exists in the cache - * - * @param string $key - * @return bool - */ - public function exists(string $key): bool - { - return apcu_exists($this->key($key)); - } + /** + * Determines if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function exists(string $key): bool + { + return apcu_exists($this->key($key)); + } - /** - * Flushes the entire cache and returns - * whether the operation was successful - * - * @return bool - */ - public function flush(): bool - { - if (empty($this->options['prefix']) === false) { - return apcu_delete(new APCUIterator('!^' . preg_quote($this->options['prefix']) . '!')); - } else { - return apcu_clear_cache(); - } - } + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + if (empty($this->options['prefix']) === false) { + return apcu_delete(new APCUIterator('!^' . preg_quote($this->options['prefix']) . '!')); + } else { + return apcu_clear_cache(); + } + } - /** - * Removes an item from the cache and returns - * whether the operation was successful - * - * @param string $key - * @return bool - */ - public function remove(string $key): bool - { - return apcu_delete($this->key($key)); - } + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + return apcu_delete($this->key($key)); + } - /** - * Internal method to retrieve the raw cache value; - * needs to return a Value object or null if not found - * - * @param string $key - * @return \Kirby\Cache\Value|null - */ - public function retrieve(string $key) - { - return Value::fromJson(apcu_fetch($this->key($key))); - } + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return Value::fromJson(apcu_fetch($this->key($key))); + } - /** - * Writes an item to the cache for a given number of minutes and - * returns whether the operation was successful - * - * - * // put an item in the cache for 15 minutes - * $cache->set('value', 'my value', 15); - * - * - * @param string $key - * @param mixed $value - * @param int $minutes - * @return bool - */ - public function set(string $key, $value, int $minutes = 0): bool - { - return apcu_store($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes)); - } + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + return apcu_store($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes)); + } } diff --git a/kirby/src/Cache/Cache.php b/kirby/src/Cache/Cache.php index 0ffda81..5eeaec8 100644 --- a/kirby/src/Cache/Cache.php +++ b/kirby/src/Cache/Cache.php @@ -16,227 +16,227 @@ namespace Kirby\Cache; */ abstract class Cache { - /** - * Stores all options for the driver - * @var array - */ - protected $options = []; + /** + * Stores all options for the driver + * @var array + */ + protected $options = []; - /** - * Sets all parameters which are needed to connect to the cache storage - * - * @param array $options - */ - public function __construct(array $options = []) - { - $this->options = $options; - } + /** + * Sets all parameters which are needed to connect to the cache storage + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + } - /** - * Writes an item to the cache for a given number of minutes and - * returns whether the operation was successful; - * this needs to be defined by the driver - * - * - * // put an item in the cache for 15 minutes - * $cache->set('value', 'my value', 15); - * - * - * @param string $key - * @param mixed $value - * @param int $minutes - * @return bool - */ - abstract public function set(string $key, $value, int $minutes = 0): bool; + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful; + * this needs to be defined by the driver + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + abstract public function set(string $key, $value, int $minutes = 0): bool; - /** - * Adds the prefix to the key if given - * - * @param string $key - * @return string - */ - protected function key(string $key): string - { - if (empty($this->options['prefix']) === false) { - $key = $this->options['prefix'] . '/' . $key; - } + /** + * Adds the prefix to the key if given + * + * @param string $key + * @return string + */ + protected function key(string $key): string + { + if (empty($this->options['prefix']) === false) { + $key = $this->options['prefix'] . '/' . $key; + } - return $key; - } + return $key; + } - /** - * Internal method to retrieve the raw cache value; - * needs to return a Value object or null if not found; - * this needs to be defined by the driver - * - * @param string $key - * @return \Kirby\Cache\Value|null - */ - abstract public function retrieve(string $key); + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found; + * this needs to be defined by the driver + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + abstract public function retrieve(string $key); - /** - * Gets an item from the cache - * - * - * // get an item from the cache driver - * $value = $cache->get('value'); - * - * // return a default value if the requested item isn't cached - * $value = $cache->get('value', 'default value'); - * - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function get(string $key, $default = null) - { - // get the Value - $value = $this->retrieve($key); + /** + * Gets an item from the cache + * + * + * // get an item from the cache driver + * $value = $cache->get('value'); + * + * // return a default value if the requested item isn't cached + * $value = $cache->get('value', 'default value'); + * + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function get(string $key, $default = null) + { + // get the Value + $value = $this->retrieve($key); - // check for a valid cache value - if (!is_a($value, 'Kirby\Cache\Value')) { - return $default; - } + // check for a valid cache value + if (!is_a($value, 'Kirby\Cache\Value')) { + return $default; + } - // remove the item if it is expired - if ($value->expires() > 0 && time() >= $value->expires()) { - $this->remove($key); - return $default; - } + // remove the item if it is expired + if ($value->expires() > 0 && time() >= $value->expires()) { + $this->remove($key); + return $default; + } - // return the pure value - return $value->value(); - } + // return the pure value + return $value->value(); + } - /** - * Calculates the expiration timestamp - * - * @param int $minutes - * @return int - */ - protected function expiration(int $minutes = 0): int - { - // 0 = keep forever - if ($minutes === 0) { - return 0; - } + /** + * Calculates the expiration timestamp + * + * @param int $minutes + * @return int + */ + protected function expiration(int $minutes = 0): int + { + // 0 = keep forever + if ($minutes === 0) { + return 0; + } - // calculate the time - return time() + ($minutes * 60); - } + // calculate the time + return time() + ($minutes * 60); + } - /** - * Checks when an item in the cache expires; - * returns the expiry timestamp on success, null if the - * item never expires and false if the item does not exist - * - * @param string $key - * @return int|null|false - */ - public function expires(string $key) - { - // get the Value object - $value = $this->retrieve($key); + /** + * Checks when an item in the cache expires; + * returns the expiry timestamp on success, null if the + * item never expires and false if the item does not exist + * + * @param string $key + * @return int|null|false + */ + public function expires(string $key) + { + // get the Value object + $value = $this->retrieve($key); - // check for a valid Value object - if (!is_a($value, 'Kirby\Cache\Value')) { - return false; - } + // check for a valid Value object + if (!is_a($value, 'Kirby\Cache\Value')) { + return false; + } - // return the expires timestamp - return $value->expires(); - } + // return the expires timestamp + return $value->expires(); + } - /** - * Checks if an item in the cache is expired - * - * @param string $key - * @return bool - */ - public function expired(string $key): bool - { - $expires = $this->expires($key); + /** + * Checks if an item in the cache is expired + * + * @param string $key + * @return bool + */ + public function expired(string $key): bool + { + $expires = $this->expires($key); - if ($expires === null) { - return false; - } elseif (!is_int($expires)) { - return true; - } else { - return time() >= $expires; - } - } + if ($expires === null) { + return false; + } elseif (!is_int($expires)) { + return true; + } else { + return time() >= $expires; + } + } - /** - * Checks when the cache has been created; - * returns the creation timestamp on success - * and false if the item does not exist - * - * @param string $key - * @return int|false - */ - public function created(string $key) - { - // get the Value object - $value = $this->retrieve($key); + /** + * Checks when the cache has been created; + * returns the creation timestamp on success + * and false if the item does not exist + * + * @param string $key + * @return int|false + */ + public function created(string $key) + { + // get the Value object + $value = $this->retrieve($key); - // check for a valid Value object - if (!is_a($value, 'Kirby\Cache\Value')) { - return false; - } + // check for a valid Value object + if (!is_a($value, 'Kirby\Cache\Value')) { + return false; + } - // return the expires timestamp - return $value->created(); - } + // return the expires timestamp + return $value->created(); + } - /** - * Alternate version for Cache::created($key) - * - * @param string $key - * @return int|false - */ - public function modified(string $key) - { - return static::created($key); - } + /** + * Alternate version for Cache::created($key) + * + * @param string $key + * @return int|false + */ + public function modified(string $key) + { + return static::created($key); + } - /** - * Determines if an item exists in the cache - * - * @param string $key - * @return bool - */ - public function exists(string $key): bool - { - return $this->expired($key) === false; - } + /** + * Determines if an item exists in the cache + * + * @param string $key + * @return bool + */ + public function exists(string $key): bool + { + return $this->expired($key) === false; + } - /** - * Removes an item from the cache and returns - * whether the operation was successful; - * this needs to be defined by the driver - * - * @param string $key - * @return bool - */ - abstract public function remove(string $key): bool; + /** + * Removes an item from the cache and returns + * whether the operation was successful; + * this needs to be defined by the driver + * + * @param string $key + * @return bool + */ + abstract public function remove(string $key): bool; - /** - * Flushes the entire cache and returns - * whether the operation was successful; - * this needs to be defined by the driver - * - * @return bool - */ - abstract public function flush(): bool; + /** + * Flushes the entire cache and returns + * whether the operation was successful; + * this needs to be defined by the driver + * + * @return bool + */ + abstract public function flush(): bool; - /** - * Returns all passed cache options - * - * @return array - */ - public function options(): array - { - return $this->options; - } + /** + * Returns all passed cache options + * + * @return array + */ + public function options(): array + { + return $this->options; + } } diff --git a/kirby/src/Cache/FileCache.php b/kirby/src/Cache/FileCache.php index ed12a69..4063263 100644 --- a/kirby/src/Cache/FileCache.php +++ b/kirby/src/Cache/FileCache.php @@ -18,217 +18,217 @@ use Kirby\Toolkit\Str; */ class FileCache extends Cache { - /** - * Full root including prefix - * - * @var string - */ - protected $root; + /** + * Full root including prefix + * + * @var string + */ + protected $root; - /** - * Sets all parameters which are needed for the file cache - * - * @param array $options 'root' (required) - * 'prefix' (default: none) - * 'extension' (file extension for cache files, default: none) - */ - public function __construct(array $options) - { - $defaults = [ - 'root' => null, - 'prefix' => null, - 'extension' => null - ]; + /** + * Sets all parameters which are needed for the file cache + * + * @param array $options 'root' (required) + * 'prefix' (default: none) + * 'extension' (file extension for cache files, default: none) + */ + public function __construct(array $options) + { + $defaults = [ + 'root' => null, + 'prefix' => null, + 'extension' => null + ]; - parent::__construct(array_merge($defaults, $options)); + parent::__construct(array_merge($defaults, $options)); - // build the full root including prefix - $this->root = $this->options['root']; - if (empty($this->options['prefix']) === false) { - $this->root .= '/' . $this->options['prefix']; - } + // build the full root including prefix + $this->root = $this->options['root']; + if (empty($this->options['prefix']) === false) { + $this->root .= '/' . $this->options['prefix']; + } - // try to create the directory - Dir::make($this->root, true); - } + // try to create the directory + Dir::make($this->root, true); + } - /** - * Returns the full root including prefix - * - * @return string - */ - public function root(): string - { - return $this->root; - } + /** + * Returns the full root including prefix + * + * @return string + */ + public function root(): string + { + return $this->root; + } - /** - * Returns the full path to a file for a given key - * - * @param string $key - * @return string - */ - protected function file(string $key): string - { - // strip out invalid characters in each path segment - // split by slash or backslash - $keyParts = []; - foreach (preg_split('#([\/\\\\])#', $key, 0, PREG_SPLIT_DELIM_CAPTURE) as $part) { - switch ($part) { - // forward slashes don't need special treatment - case '/': - break; + /** + * Returns the full path to a file for a given key + * + * @param string $key + * @return string + */ + protected function file(string $key): string + { + // strip out invalid characters in each path segment + // split by slash or backslash + $keyParts = []; + foreach (preg_split('#([\/\\\\])#', $key, 0, PREG_SPLIT_DELIM_CAPTURE) as $part) { + switch ($part) { + case '/': + // forward slashes don't need special treatment + break; - // backslashes get their own marker in the path - // to differentiate the cache key from one with forward slashes - case '\\': - $keyParts[] = '_backslash'; - break; + case '\\': + // backslashes get their own marker in the path + // to differentiate the cache key from one with forward slashes + $keyParts[] = '_backslash'; + break; - // empty part means two slashes in a row; - // special marker like for backslashes - case '': - $keyParts[] = '_empty'; - break; + case '': + // empty part means two slashes in a row; + // special marker like for backslashes + $keyParts[] = '_empty'; + break; - // an actual path segment - default: - // check if the segment only contains safe characters; - // underscores are *not* safe to guarantee uniqueness - // as they are used in the special cases - if (preg_match('/^[a-zA-Z0-9-]+$/', $part) === 1) { - $keyParts[] = $part; - } else { - $keyParts[] = Str::slug($part) . '_' . sha1($part); - } - } - } + default: + // an actual path segment: + // check if the segment only contains safe characters; + // underscores are *not* safe to guarantee uniqueness + // as they are used in the special cases + if (preg_match('/^[a-zA-Z0-9-]+$/', $part) === 1) { + $keyParts[] = $part; + } else { + $keyParts[] = Str::slug($part) . '_' . sha1($part); + } + } + } - $file = $this->root . '/' . implode('/', $keyParts); + $file = $this->root . '/' . implode('/', $keyParts); - if (isset($this->options['extension'])) { - return $file . '.' . $this->options['extension']; - } else { - return $file; - } - } + if (isset($this->options['extension'])) { + return $file . '.' . $this->options['extension']; + } else { + return $file; + } + } - /** - * Writes an item to the cache for a given number of minutes and - * returns whether the operation was successful - * - * - * // put an item in the cache for 15 minutes - * $cache->set('value', 'my value', 15); - * - * - * @param string $key - * @param mixed $value - * @param int $minutes - * @return bool - */ - public function set(string $key, $value, int $minutes = 0): bool - { - $file = $this->file($key); + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + $file = $this->file($key); - return F::write($file, (new Value($value, $minutes))->toJson()); - } + return F::write($file, (new Value($value, $minutes))->toJson()); + } - /** - * Internal method to retrieve the raw cache value; - * needs to return a Value object or null if not found - * - * @param string $key - * @return \Kirby\Cache\Value|null - */ - public function retrieve(string $key) - { - $file = $this->file($key); - $value = F::read($file); + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + $file = $this->file($key); + $value = F::read($file); - return $value ? Value::fromJson($value) : null; - } + return $value ? Value::fromJson($value) : null; + } - /** - * Checks when the cache has been created; - * returns the creation timestamp on success - * and false if the item does not exist - * - * @param string $key - * @return mixed - */ - public function created(string $key) - { - // use the modification timestamp - // as indicator when the cache has been created/overwritten - clearstatcache(); + /** + * Checks when the cache has been created; + * returns the creation timestamp on success + * and false if the item does not exist + * + * @param string $key + * @return mixed + */ + public function created(string $key) + { + // use the modification timestamp + // as indicator when the cache has been created/overwritten + clearstatcache(); - // get the file for this cache key - $file = $this->file($key); - return file_exists($file) ? filemtime($this->file($key)) : false; - } + // get the file for this cache key + $file = $this->file($key); + return file_exists($file) ? filemtime($this->file($key)) : false; + } - /** - * Removes an item from the cache and returns - * whether the operation was successful - * - * @param string $key - * @return bool - */ - public function remove(string $key): bool - { - $file = $this->file($key); + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + $file = $this->file($key); - if (is_file($file) === true && F::remove($file) === true) { - $this->removeEmptyDirectories(dirname($file)); - return true; - } + if (is_file($file) === true && F::remove($file) === true) { + $this->removeEmptyDirectories(dirname($file)); + return true; + } - return false; - } + return false; + } - /** - * Removes empty directories safely by checking each directory - * up to the root directory - * - * @param string $dir - * @return void - */ - protected function removeEmptyDirectories(string $dir): void - { - try { - // ensure the path doesn't end with a slash for the next comparison - $dir = rtrim($dir, '/\/'); + /** + * Removes empty directories safely by checking each directory + * up to the root directory + * + * @param string $dir + * @return void + */ + protected function removeEmptyDirectories(string $dir): void + { + try { + // ensure the path doesn't end with a slash for the next comparison + $dir = rtrim($dir, '/\/'); - // checks all directory segments until reaching the root directory - while (Str::startsWith($dir, $this->root()) === true && $dir !== $this->root()) { - $files = array_diff(scandir($dir) ?? [], ['.', '..']); + // checks all directory segments until reaching the root directory + while (Str::startsWith($dir, $this->root()) === true && $dir !== $this->root()) { + $files = array_diff(scandir($dir) ?? [], ['.', '..']); - if (empty($files) === true && Dir::remove($dir) === true) { - // continue with the next level up - $dir = dirname($dir); - } else { - // no need to continue with the next level up as `$dir` was not deleted - break; - } - } - } catch (Exception $e) { // @codeCoverageIgnore - // silently stops the process - } - } + if (empty($files) === true && Dir::remove($dir) === true) { + // continue with the next level up + $dir = dirname($dir); + } else { + // no need to continue with the next level up as `$dir` was not deleted + break; + } + } + } catch (Exception $e) { // @codeCoverageIgnore + // silently stops the process + } + } - /** - * Flushes the entire cache and returns - * whether the operation was successful - * - * @return bool - */ - public function flush(): bool - { - if (Dir::remove($this->root) === true && Dir::make($this->root) === true) { - return true; - } + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + if (Dir::remove($this->root) === true && Dir::make($this->root) === true) { + return true; + } - return false; // @codeCoverageIgnore - } + return false; // @codeCoverageIgnore + } } diff --git a/kirby/src/Cache/MemCached.php b/kirby/src/Cache/MemCached.php index de83a85..591919f 100644 --- a/kirby/src/Cache/MemCached.php +++ b/kirby/src/Cache/MemCached.php @@ -15,85 +15,85 @@ use Memcached as MemcachedExt; */ class MemCached extends Cache { - /** - * store for the memcache connection - * @var \Memcached - */ - protected $connection; + /** + * store for the memcache connection + * @var \Memcached + */ + protected $connection; - /** - * Sets all parameters which are needed to connect to Memcached - * - * @param array $options 'host' (default: localhost) - * 'port' (default: 11211) - * 'prefix' (default: null) - */ - public function __construct(array $options = []) - { - $defaults = [ - 'host' => 'localhost', - 'port' => 11211, - 'prefix' => null, - ]; + /** + * Sets all parameters which are needed to connect to Memcached + * + * @param array $options 'host' (default: localhost) + * 'port' (default: 11211) + * 'prefix' (default: null) + */ + public function __construct(array $options = []) + { + $defaults = [ + 'host' => 'localhost', + 'port' => 11211, + 'prefix' => null, + ]; - parent::__construct(array_merge($defaults, $options)); + parent::__construct(array_merge($defaults, $options)); - $this->connection = new MemcachedExt(); - $this->connection->addServer($this->options['host'], $this->options['port']); - } + $this->connection = new MemcachedExt(); + $this->connection->addServer($this->options['host'], $this->options['port']); + } - /** - * Writes an item to the cache for a given number of minutes and - * returns whether the operation was successful - * - * - * // put an item in the cache for 15 minutes - * $cache->set('value', 'my value', 15); - * - * - * @param string $key - * @param mixed $value - * @param int $minutes - * @return bool - */ - public function set(string $key, $value, int $minutes = 0): bool - { - return $this->connection->set($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes)); - } + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + return $this->connection->set($this->key($key), (new Value($value, $minutes))->toJson(), $this->expiration($minutes)); + } - /** - * Internal method to retrieve the raw cache value; - * needs to return a Value object or null if not found - * - * @param string $key - * @return \Kirby\Cache\Value|null - */ - public function retrieve(string $key) - { - return Value::fromJson($this->connection->get($this->key($key))); - } + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return Value::fromJson($this->connection->get($this->key($key))); + } - /** - * Removes an item from the cache and returns - * whether the operation was successful - * - * @param string $key - * @return bool - */ - public function remove(string $key): bool - { - return $this->connection->delete($this->key($key)); - } + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + return $this->connection->delete($this->key($key)); + } - /** - * Flushes the entire cache and returns - * whether the operation was successful; - * WARNING: Memcached only supports flushing the whole cache at once! - * - * @return bool - */ - public function flush(): bool - { - return $this->connection->flush(); - } + /** + * Flushes the entire cache and returns + * whether the operation was successful; + * WARNING: Memcached only supports flushing the whole cache at once! + * + * @return bool + */ + public function flush(): bool + { + return $this->connection->flush(); + } } diff --git a/kirby/src/Cache/MemoryCache.php b/kirby/src/Cache/MemoryCache.php index 5b0d40d..3ee6636 100644 --- a/kirby/src/Cache/MemoryCache.php +++ b/kirby/src/Cache/MemoryCache.php @@ -13,70 +13,70 @@ namespace Kirby\Cache; */ class MemoryCache extends Cache { - /** - * Cache data - * @var array - */ - protected $store = []; + /** + * Cache data + * @var array + */ + protected $store = []; - /** - * Writes an item to the cache for a given number of minutes and - * returns whether the operation was successful - * - * - * // put an item in the cache for 15 minutes - * $cache->set('value', 'my value', 15); - * - * - * @param string $key - * @param mixed $value - * @param int $minutes - * @return bool - */ - public function set(string $key, $value, int $minutes = 0): bool - { - $this->store[$key] = new Value($value, $minutes); - return true; - } + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + $this->store[$key] = new Value($value, $minutes); + return true; + } - /** - * Internal method to retrieve the raw cache value; - * needs to return a Value object or null if not found - * - * @param string $key - * @return \Kirby\Cache\Value|null - */ - public function retrieve(string $key) - { - return $this->store[$key] ?? null; - } + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return $this->store[$key] ?? null; + } - /** - * Removes an item from the cache and returns - * whether the operation was successful - * - * @param string $key - * @return bool - */ - public function remove(string $key): bool - { - if (isset($this->store[$key])) { - unset($this->store[$key]); - return true; - } else { - return false; - } - } + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + if (isset($this->store[$key])) { + unset($this->store[$key]); + return true; + } else { + return false; + } + } - /** - * Flushes the entire cache and returns - * whether the operation was successful - * - * @return bool - */ - public function flush(): bool - { - $this->store = []; - return true; - } + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + $this->store = []; + return true; + } } diff --git a/kirby/src/Cache/NullCache.php b/kirby/src/Cache/NullCache.php index 1064504..36b419e 100644 --- a/kirby/src/Cache/NullCache.php +++ b/kirby/src/Cache/NullCache.php @@ -13,57 +13,57 @@ namespace Kirby\Cache; */ class NullCache extends Cache { - /** - * Writes an item to the cache for a given number of minutes and - * returns whether the operation was successful - * - * - * // put an item in the cache for 15 minutes - * $cache->set('value', 'my value', 15); - * - * - * @param string $key - * @param mixed $value - * @param int $minutes - * @return bool - */ - public function set(string $key, $value, int $minutes = 0): bool - { - return true; - } + /** + * Writes an item to the cache for a given number of minutes and + * returns whether the operation was successful + * + * + * // put an item in the cache for 15 minutes + * $cache->set('value', 'my value', 15); + * + * + * @param string $key + * @param mixed $value + * @param int $minutes + * @return bool + */ + public function set(string $key, $value, int $minutes = 0): bool + { + return true; + } - /** - * Internal method to retrieve the raw cache value; - * needs to return a Value object or null if not found - * - * @param string $key - * @return \Kirby\Cache\Value|null - */ - public function retrieve(string $key) - { - return null; - } + /** + * Internal method to retrieve the raw cache value; + * needs to return a Value object or null if not found + * + * @param string $key + * @return \Kirby\Cache\Value|null + */ + public function retrieve(string $key) + { + return null; + } - /** - * Removes an item from the cache and returns - * whether the operation was successful - * - * @param string $key - * @return bool - */ - public function remove(string $key): bool - { - return true; - } + /** + * Removes an item from the cache and returns + * whether the operation was successful + * + * @param string $key + * @return bool + */ + public function remove(string $key): bool + { + return true; + } - /** - * Flushes the entire cache and returns - * whether the operation was successful - * - * @return bool - */ - public function flush(): bool - { - return true; - } + /** + * Flushes the entire cache and returns + * whether the operation was successful + * + * @return bool + */ + public function flush(): bool + { + return true; + } } diff --git a/kirby/src/Cache/Value.php b/kirby/src/Cache/Value.php index 075d76b..3a1105e 100644 --- a/kirby/src/Cache/Value.php +++ b/kirby/src/Cache/Value.php @@ -17,136 +17,136 @@ use Throwable; */ class Value { - /** - * Cached value - * @var mixed - */ - protected $value; + /** + * Cached value + * @var mixed + */ + protected $value; - /** - * the number of minutes until the value expires - * @todo Rename this property to $expiry to reflect - * both minutes and absolute timestamps - * @var int - */ - protected $minutes; + /** + * the number of minutes until the value expires + * @todo Rename this property to $expiry to reflect + * both minutes and absolute timestamps + * @var int + */ + protected $minutes; - /** - * Creation timestamp - * @var int - */ - protected $created; + /** + * Creation timestamp + * @var int + */ + protected $created; - /** - * Constructor - * - * @param mixed $value - * @param int $minutes the number of minutes until the value expires - * or an absolute UNIX timestamp - * @param int $created the UNIX timestamp when the value has been created - */ - public function __construct($value, int $minutes = 0, int $created = null) - { - $this->value = $value; - $this->minutes = $minutes ?? 0; - $this->created = $created ?? time(); - } + /** + * Constructor + * + * @param mixed $value + * @param int $minutes the number of minutes until the value expires + * or an absolute UNIX timestamp + * @param int $created the UNIX timestamp when the value has been created + */ + public function __construct($value, int $minutes = 0, int $created = null) + { + $this->value = $value; + $this->minutes = $minutes ?? 0; + $this->created = $created ?? time(); + } - /** - * Returns the creation date as UNIX timestamp - * - * @return int - */ - public function created(): int - { - return $this->created; - } + /** + * Returns the creation date as UNIX timestamp + * + * @return int + */ + public function created(): int + { + return $this->created; + } - /** - * Returns the expiration date as UNIX timestamp or - * null if the value never expires - * - * @return int|null - */ - public function expires(): ?int - { - // 0 = keep forever - if ($this->minutes === 0) { - return null; - } + /** + * Returns the expiration date as UNIX timestamp or + * null if the value never expires + * + * @return int|null + */ + public function expires(): ?int + { + // 0 = keep forever + if ($this->minutes === 0) { + return null; + } - if ($this->minutes > 1000000000) { - // absolute timestamp - return $this->minutes; - } + if ($this->minutes > 1000000000) { + // absolute timestamp + return $this->minutes; + } - return $this->created + ($this->minutes * 60); - } + return $this->created + ($this->minutes * 60); + } - /** - * Creates a value object from an array - * - * @param array $array - * @return static - */ - public static function fromArray(array $array) - { - return new static($array['value'] ?? null, $array['minutes'] ?? 0, $array['created'] ?? null); - } + /** + * Creates a value object from an array + * + * @param array $array + * @return static + */ + public static function fromArray(array $array) + { + return new static($array['value'] ?? null, $array['minutes'] ?? 0, $array['created'] ?? null); + } - /** - * Creates a value object from a JSON string; - * returns null on error - * - * @param string $json - * @return static|null - */ - public static function fromJson(string $json) - { - try { - $array = json_decode($json, true); + /** + * Creates a value object from a JSON string; + * returns null on error + * + * @param string $json + * @return static|null + */ + public static function fromJson(string $json) + { + try { + $array = json_decode($json, true); - if (is_array($array)) { - return static::fromArray($array); - } else { - return null; - } - } catch (Throwable $e) { - return null; - } - } + if (is_array($array)) { + return static::fromArray($array); + } else { + return null; + } + } catch (Throwable $e) { + return null; + } + } - /** - * Converts the object to a JSON string - * - * @return string - */ - public function toJson(): string - { - return json_encode($this->toArray()); - } + /** + * Converts the object to a JSON string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } - /** - * Converts the object to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'created' => $this->created, - 'minutes' => $this->minutes, - 'value' => $this->value, - ]; - } + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'created' => $this->created, + 'minutes' => $this->minutes, + 'value' => $this->value, + ]; + } - /** - * Returns the pure value - * - * @return mixed - */ - public function value() - { - return $this->value; - } + /** + * Returns the pure value + * + * @return mixed + */ + public function value() + { + return $this->value; + } } diff --git a/kirby/src/Cms/Api.php b/kirby/src/Cms/Api.php index fdd9bf7..30aa140 100644 --- a/kirby/src/Cms/Api.php +++ b/kirby/src/Cms/Api.php @@ -17,229 +17,229 @@ use Kirby\Form\Form; */ class Api extends BaseApi { - /** - * @var App - */ - protected $kirby; + /** + * @var App + */ + protected $kirby; - /** - * Execute an API call for the given path, - * request method and optional request data - * - * @param string|null $path - * @param string $method - * @param array $requestData - * @return mixed - */ - public function call(string $path = null, string $method = 'GET', array $requestData = []) - { - $this->setRequestMethod($method); - $this->setRequestData($requestData); + /** + * Execute an API call for the given path, + * request method and optional request data + * + * @param string|null $path + * @param string $method + * @param array $requestData + * @return mixed + */ + public function call(string $path = null, string $method = 'GET', array $requestData = []) + { + $this->setRequestMethod($method); + $this->setRequestData($requestData); - $this->kirby->setCurrentLanguage($this->language()); + $this->kirby->setCurrentLanguage($this->language()); - $allowImpersonation = $this->kirby()->option('api.allowImpersonation', false); - if ($user = $this->kirby->user(null, $allowImpersonation)) { - $translation = $user->language(); - } else { - $translation = $this->kirby->panelLanguage(); - } - $this->kirby->setCurrentTranslation($translation); + $allowImpersonation = $this->kirby()->option('api.allowImpersonation', false); + if ($user = $this->kirby->user(null, $allowImpersonation)) { + $translation = $user->language(); + } else { + $translation = $this->kirby->panelLanguage(); + } + $this->kirby->setCurrentTranslation($translation); - return parent::call($path, $method, $requestData); - } + return parent::call($path, $method, $requestData); + } - /** - * @param mixed $model - * @param string $name - * @param string|null $path - * @return mixed - * @throws \Kirby\Exception\NotFoundException if the field type cannot be found or the field cannot be loaded - */ - public function fieldApi($model, string $name, string $path = null) - { - $field = Form::for($model)->field($name); + /** + * @param mixed $model + * @param string $name + * @param string|null $path + * @return mixed + * @throws \Kirby\Exception\NotFoundException if the field type cannot be found or the field cannot be loaded + */ + public function fieldApi($model, string $name, string $path = null) + { + $field = Form::for($model)->field($name); - $fieldApi = new static( - array_merge($this->propertyData, [ - 'data' => array_merge($this->data(), ['field' => $field]), - 'routes' => $field->api(), - ]), - ); + $fieldApi = new static( + array_merge($this->propertyData, [ + 'data' => array_merge($this->data(), ['field' => $field]), + 'routes' => $field->api(), + ]), + ); - return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); - } + return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); + } - /** - * Returns the file object for the given - * parent path and filename - * - * @param string|null $path Path to file's parent model - * @param string $filename Filename - * @return \Kirby\Cms\File|null - * @throws \Kirby\Exception\NotFoundException if the file cannot be found - */ - public function file(string $path = null, string $filename) - { - return Find::file($path, $filename); - } + /** + * Returns the file object for the given + * parent path and filename + * + * @param string|null $path Path to file's parent model + * @param string $filename Filename + * @return \Kirby\Cms\File|null + * @throws \Kirby\Exception\NotFoundException if the file cannot be found + */ + public function file(string $path = null, string $filename) + { + return Find::file($path, $filename); + } - /** - * Returns the model's object for the given path - * - * @param string $path Path to parent model - * @return \Kirby\Cms\Model|null - * @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid - * @throws \Kirby\Exception\NotFoundException if the model cannot be found - */ - public function parent(string $path) - { - return Find::parent($path); - } + /** + * Returns the model's object for the given path + * + * @param string $path Path to parent model + * @return \Kirby\Cms\Model|null + * @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid + * @throws \Kirby\Exception\NotFoundException if the model cannot be found + */ + public function parent(string $path) + { + return Find::parent($path); + } - /** - * Returns the Kirby instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return $this->kirby; - } + /** + * Returns the Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->kirby; + } - /** - * Returns the language request header - * - * @return string|null - */ - public function language(): ?string - { - return get('language') ?? $this->requestHeaders('x-language'); - } + /** + * Returns the language request header + * + * @return string|null + */ + public function language(): ?string + { + return $this->requestQuery('language') ?? $this->requestHeaders('x-language'); + } - /** - * Returns the page object for the given id - * - * @param string $id Page's id - * @return \Kirby\Cms\Page|null - * @throws \Kirby\Exception\NotFoundException if the page cannot be found - */ - public function page(string $id) - { - return Find::page($id); - } + /** + * Returns the page object for the given id + * + * @param string $id Page's id + * @return \Kirby\Cms\Page|null + * @throws \Kirby\Exception\NotFoundException if the page cannot be found + */ + public function page(string $id) + { + return Find::page($id); + } - /** - * Returns the subpages for the given - * parent. The subpages can be filtered - * by status (draft, listed, unlisted, published, all) - * - * @param string|null $parentId - * @param string|null $status - * @return \Kirby\Cms\Pages - */ - public function pages(string $parentId = null, string $status = null) - { - $parent = $parentId === null ? $this->site() : $this->page($parentId); + /** + * Returns the subpages for the given + * parent. The subpages can be filtered + * by status (draft, listed, unlisted, published, all) + * + * @param string|null $parentId + * @param string|null $status + * @return \Kirby\Cms\Pages + */ + public function pages(string $parentId = null, string $status = null) + { + $parent = $parentId === null ? $this->site() : $this->page($parentId); - switch ($status) { - case 'all': - return $parent->childrenAndDrafts(); - case 'draft': - case 'drafts': - return $parent->drafts(); - case 'listed': - return $parent->children()->listed(); - case 'unlisted': - return $parent->children()->unlisted(); - case 'published': - default: - return $parent->children(); - } - } + switch ($status) { + case 'all': + return $parent->childrenAndDrafts(); + case 'draft': + case 'drafts': + return $parent->drafts(); + case 'listed': + return $parent->children()->listed(); + case 'unlisted': + return $parent->children()->unlisted(); + case 'published': + default: + return $parent->children(); + } + } - /** - * Search for direct subpages of the - * given parent - * - * @param string|null $parent - * @return \Kirby\Cms\Pages - */ - public function searchPages(string $parent = null) - { - $pages = $this->pages($parent, $this->requestQuery('status')); + /** + * Search for direct subpages of the + * given parent + * + * @param string|null $parent + * @return \Kirby\Cms\Pages + */ + public function searchPages(string $parent = null) + { + $pages = $this->pages($parent, $this->requestQuery('status')); - if ($this->requestMethod() === 'GET') { - return $pages->search($this->requestQuery('q')); - } + if ($this->requestMethod() === 'GET') { + return $pages->search($this->requestQuery('q')); + } - return $pages->query($this->requestBody()); - } + return $pages->query($this->requestBody()); + } - /** - * Returns the current Session instance - * - * @param array $options Additional options, see the session component - * @return \Kirby\Session\Session - */ - public function session(array $options = []) - { - return $this->kirby->session(array_merge([ - 'detect' => true - ], $options)); - } + /** + * Returns the current Session instance + * + * @param array $options Additional options, see the session component + * @return \Kirby\Session\Session + */ + public function session(array $options = []) + { + return $this->kirby->session(array_merge([ + 'detect' => true + ], $options)); + } - /** - * Setter for the parent Kirby instance - * - * @param \Kirby\Cms\App $kirby - * @return $this - */ - protected function setKirby(App $kirby) - { - $this->kirby = $kirby; - return $this; - } + /** + * Setter for the parent Kirby instance + * + * @param \Kirby\Cms\App $kirby + * @return $this + */ + protected function setKirby(App $kirby) + { + $this->kirby = $kirby; + return $this; + } - /** - * Returns the site object - * - * @return \Kirby\Cms\Site - */ - public function site() - { - return $this->kirby->site(); - } + /** + * Returns the site object + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->kirby->site(); + } - /** - * Returns the user object for the given id or - * returns the current authenticated user if no - * id is passed - * - * @param string|null $id User's id - * @return \Kirby\Cms\User|null - * @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found - */ - public function user(string $id = null) - { - try { - return Find::user($id); - } catch (NotFoundException $e) { - if ($id === null) { - return null; - } + /** + * Returns the user object for the given id or + * returns the current authenticated user if no + * id is passed + * + * @param string|null $id User's id + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found + */ + public function user(string $id = null) + { + try { + return Find::user($id); + } catch (NotFoundException $e) { + if ($id === null) { + return null; + } - throw $e; - } - } + throw $e; + } + } - /** - * Returns the users collection - * - * @return \Kirby\Cms\Users - */ - public function users() - { - return $this->kirby->users(); - } + /** + * Returns the users collection + * + * @return \Kirby\Cms\Users + */ + public function users() + { + return $this->kirby->users(); + } } diff --git a/kirby/src/Cms/App.php b/kirby/src/Cms/App.php index a2ac120..04cb4be 100644 --- a/kirby/src/Cms/App.php +++ b/kirby/src/Cms/App.php @@ -9,9 +9,10 @@ use Kirby\Exception\LogicException; use Kirby\Exception\NotFoundException; use Kirby\Filesystem\Dir; use Kirby\Filesystem\F; +use Kirby\Http\Environment; use Kirby\Http\Request; +use Kirby\Http\Response; use Kirby\Http\Router; -use Kirby\Http\Server; use Kirby\Http\Uri; use Kirby\Http\Visitor; use Kirby\Session\AutoSession; @@ -21,6 +22,7 @@ use Kirby\Toolkit\A; use Kirby\Toolkit\Config; use Kirby\Toolkit\Controller; use Kirby\Toolkit\Properties; +use Kirby\Toolkit\Str; use Throwable; /** @@ -38,1622 +40,1769 @@ use Throwable; */ class App { - use AppCaches; - use AppErrors; - use AppPlugins; - use AppTranslations; - use AppUsers; - use Properties; - - public const CLASS_ALIAS = 'kirby'; - - protected static $instance; - protected static $version; - - public $data = []; - - protected $api; - protected $collections; - protected $core; - protected $defaultLanguage; - protected $environment; - protected $language; - protected $languages; - protected $locks; - protected $multilang; - protected $nonce; - protected $options; - protected $path; - protected $request; - protected $response; - protected $roles; - protected $roots; - protected $routes; - protected $router; - protected $server; - protected $sessionHandler; - protected $site; - protected $system; - protected $urls; - protected $user; - protected $users; - protected $visitor; - - /** - * Creates a new App instance - * - * @param array $props - * @param bool $setInstance If false, the instance won't be set globally - */ - public function __construct(array $props = [], bool $setInstance = true) - { - $this->core = new Core($this); - - // register all roots to be able to load stuff afterwards - $this->bakeRoots($props['roots'] ?? []); - - try { - // stuff from config and additional options - $this->optionsFromConfig(); - $this->optionsFromProps($props['options'] ?? []); - $this->optionsFromEnvironment(); - } finally { - // register the Whoops error handler inside of a - // try-finally block to ensure it's still registered - // even if there is a problem loading the configurations - $this->handleErrors(); - } - - // set the path to make it available for the url bakery - $this->setPath($props['path'] ?? null); - - // create all urls after the config, so possible - // options can be taken into account - $this->bakeUrls($props['urls'] ?? []); - - // configurable properties - $this->setOptionalProperties($props, [ - 'languages', - 'request', - 'roles', - 'site', - 'user', - 'users' - ]); - - // set the singleton - if (static::$instance === null || $setInstance === true) { - Model::$kirby = static::$instance = $this; - } - - // setup the I18n class with the translation loader - $this->i18n(); - - // load all extensions - $this->extensionsFromSystem(); - $this->extensionsFromProps($props); - $this->extensionsFromPlugins(); - $this->extensionsFromOptions(); - $this->extensionsFromFolders(); - - // trigger hook for use in plugins - $this->trigger('system.loadPlugins:after'); - - // execute a ready callback from the config - $this->optionsFromReadyCallback(); - - // bake config - $this->bakeOptions(); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return [ - 'languages' => $this->languages(), - 'options' => $this->options(), - 'request' => $this->request(), - 'roots' => $this->roots(), - 'site' => $this->site(), - 'urls' => $this->urls(), - 'version' => $this->version(), - ]; - } - - /** - * Returns the Api instance - * - * @internal - * @return \Kirby\Cms\Api - */ - public function api() - { - if ($this->api !== null) { - return $this->api; - } - - $root = $this->root('kirby') . '/config/api'; - $extensions = $this->extensions['api'] ?? []; - $routes = (include $root . '/routes.php')($this); - - $api = [ - 'debug' => $this->option('debug', false), - 'authentication' => $extensions['authentication'] ?? include $root . '/authentication.php', - 'data' => $extensions['data'] ?? [], - 'collections' => array_merge($extensions['collections'] ?? [], include $root . '/collections.php'), - 'models' => array_merge($extensions['models'] ?? [], include $root . '/models.php'), - 'routes' => array_merge($routes, $extensions['routes'] ?? []), - 'kirby' => $this, - ]; - - return $this->api = new Api($api); - } - - /** - * Applies a hook to the given value - * - * @internal - * @param string $name Full event name - * @param array $args Associative array of named event arguments - * @param string $modify Key in $args that is modified by the hooks - * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) - * @return mixed Resulting value as modified by the hooks - */ - public function apply(string $name, array $args, string $modify, ?Event $originalEvent = null) - { - $event = $originalEvent ?? new Event($name, $args); - - if ($functions = $this->extension('hooks', $name)) { - foreach ($functions as $function) { - // bind the App object to the hook - $newValue = $event->call($this, $function); - - // update value if one was returned - if ($newValue !== null) { - $event->updateArgument($modify, $newValue); - } - } - } - - // apply wildcard hooks if available - $nameWildcards = $event->nameWildcards(); - if ($originalEvent === null && count($nameWildcards) > 0) { - foreach ($nameWildcards as $nameWildcard) { - // the $event object is passed by reference - // and will be modified down the chain - $this->apply($nameWildcard, $event->arguments(), $modify, $event); - } - } - - return $event->argument($modify); - } - - /** - * Normalizes and globally sets the configured options - * - * @return $this - */ - protected function bakeOptions() - { - // convert the old plugin option syntax to the new one - foreach ($this->options as $key => $value) { - // detect option keys with the `vendor.plugin.option` format - if (preg_match('/^([a-z0-9-]+\.[a-z0-9-]+)\.(.*)$/i', $key, $matches) === 1) { - list(, $plugin, $option) = $matches; - - // verify that it's really a plugin option - if (isset(static::$plugins[str_replace('.', '/', $plugin)]) !== true) { - continue; - } - - // ensure that the target option array exists - // (which it will if the plugin has any options) - if (isset($this->options[$plugin]) !== true) { - $this->options[$plugin] = []; // @codeCoverageIgnore - } - - // move the option to the plugin option array - // don't overwrite nested arrays completely but merge them - $this->options[$plugin] = array_replace_recursive($this->options[$plugin], [$option => $value]); - unset($this->options[$key]); - } - } - - Config::$data = $this->options; - return $this; - } - - /** - * Sets the directory structure - * - * @param array|null $roots - * @return $this - */ - protected function bakeRoots(array $roots = null) - { - $roots = array_merge($this->core->roots(), (array)$roots); - $this->roots = Ingredients::bake($roots); - return $this; - } - - /** - * Sets the Url structure - * - * @param array|null $urls - * @return $this - */ - protected function bakeUrls(array $urls = null) - { - $urls = array_merge($this->core->urls(), (array)$urls); - $this->urls = Ingredients::bake($urls); - return $this; - } - - /** - * Returns all available blueprints for this installation - * - * @param string $type - * @return array - */ - public function blueprints(string $type = 'pages'): array - { - $blueprints = []; - - foreach ($this->extensions('blueprints') as $name => $blueprint) { - if (dirname($name) === $type) { - $name = basename($name); - $blueprints[$name] = $name; - } - } - - foreach (glob($this->root('blueprints') . '/' . $type . '/*.yml') as $blueprint) { - $name = F::name($blueprint); - $blueprints[$name] = $name; - } - - ksort($blueprints); - - return array_values($blueprints); - } - - /** - * Calls any Kirby route - * - * @param string|null $path - * @param string|null $method - * @return mixed - */ - public function call(string $path = null, string $method = null) - { - $router = $this->router(); - - /** - * @todo Closures should not defined statically but - * for each instance to avoid this constant setting and unsetting - */ - $router::$beforeEach = function ($route, $path, $method) { - $this->trigger('route:before', compact('route', 'path', 'method')); - }; - - $router::$afterEach = function ($route, $path, $method, $result, $final) { - return $this->apply('route:after', compact('route', 'path', 'method', 'result', 'final'), 'result'); - }; - - $result = $router->call($path ?? $this->path(), $method ?? $this->request()->method()); - - $router::$beforeEach = null; - $router::$afterEach = null; - - return $result; - } - - /** - * Creates an instance with the same - * initial properties - * - * @param array $props - * @param bool $setInstance If false, the instance won't be set globally - * @return static - */ - public function clone(array $props = [], bool $setInstance = true) - { - $props = array_replace_recursive($this->propertyData, $props); - - $clone = new static($props, $setInstance); - $clone->data = $this->data; - - return $clone; - } - - /** - * Returns a specific user-defined collection - * by name. All relevant dependencies are - * automatically injected - * - * @param string $name - * @return \Kirby\Cms\Collection|null - */ - public function collection(string $name) - { - return $this->collections()->get($name, [ - 'kirby' => $this, - 'site' => $this->site(), - 'pages' => $this->site()->children(), - 'users' => $this->users() - ]); - } - - /** - * Returns all user-defined collections - * - * @return \Kirby\Cms\Collections - */ - public function collections() - { - return $this->collections = $this->collections ?? new Collections(); - } - - /** - * Returns a core component - * - * @internal - * @param string $name - * @return mixed - */ - public function component($name) - { - return $this->extensions['components'][$name] ?? null; - } - - /** - * Returns the content extension - * - * @internal - * @return string - */ - public function contentExtension(): string - { - return $this->options['content']['extension'] ?? 'txt'; - } - - /** - * Returns files that should be ignored when scanning folders - * - * @internal - * @return array - */ - public function contentIgnore(): array - { - return $this->options['content']['ignore'] ?? Dir::$ignore; - } - - /** - * Generates a non-guessable token based on model - * data and a configured salt - * - * @param mixed $model Object to pass to the salt callback if configured - * @param string $value Model data to include in the generated token - * @return string - */ - public function contentToken($model, string $value): string - { - if (method_exists($model, 'root') === true) { - $default = $model->root(); - } else { - $default = $this->root('content'); - } - - $salt = $this->option('content.salt', $default); - - if (is_a($salt, 'Closure') === true) { - $salt = $salt($model); - } - - return hash_hmac('sha1', $value, $salt); - } - - /** - * Calls a page controller by name - * and with the given arguments - * - * @internal - * @param string $name - * @param array $arguments - * @param string $contentType - * @return array - */ - public function controller(string $name, array $arguments = [], string $contentType = 'html'): array - { - $name = basename(strtolower($name)); - - if ($controller = $this->controllerLookup($name, $contentType)) { - return (array)$controller->call($this, $arguments); - } - - if ($contentType !== 'html') { - - // no luck for a specific representation controller? - // let's try the html controller instead - if ($controller = $this->controllerLookup($name)) { - return (array)$controller->call($this, $arguments); - } - } - - // still no luck? Let's take the site controller - if ($controller = $this->controllerLookup('site')) { - return (array)$controller->call($this, $arguments); - } - - return []; - } - - /** - * Try to find a controller by name - * - * @param string $name - * @param string $contentType - * @return \Kirby\Toolkit\Controller|null - */ - protected function controllerLookup(string $name, string $contentType = 'html') - { - if ($contentType !== null && $contentType !== 'html') { - $name .= '.' . $contentType; - } - - // controller on disk - if ($controller = Controller::load($this->root('controllers') . '/' . $name . '.php')) { - return $controller; - } - - // registry controller - if ($controller = $this->extension('controllers', $name)) { - return is_a($controller, 'Kirby\Toolkit\Controller') ? $controller : new Controller($controller); - } - - return null; - } - - /** - * Get access to object that lists - * all parts of Kirby core - * - * @return \Kirby\Cms\Core - */ - public function core() - { - return $this->core; - } - - /** - * Returns the default language object - * - * @return \Kirby\Cms\Language|null - */ - public function defaultLanguage() - { - return $this->defaultLanguage = $this->defaultLanguage ?? $this->languages()->default(); - } - - /** - * Destroy the instance singleton and - * purge other static props - * - * @internal - */ - public static function destroy(): void - { - static::$plugins = []; - static::$instance = null; - } - - /** - * Detect the preferred language from the visitor object - * - * @return \Kirby\Cms\Language - */ - public function detectedLanguage() - { - $languages = $this->languages(); - $visitor = $this->visitor(); - - foreach ($visitor->acceptedLanguages() as $lang) { - if ($language = $languages->findBy('locale', $lang->locale(LC_ALL))) { - return $language; - } - } - - foreach ($visitor->acceptedLanguages() as $lang) { - if ($language = $languages->findBy('code', $lang->code())) { - return $language; - } - } - - return $this->defaultLanguage(); - } - - /** - * Returns the Email singleton - * - * @param mixed $preset - * @param array $props - * @return \Kirby\Email\Email - */ - public function email($preset = [], array $props = []) - { - $debug = $props['debug'] ?? false; - $props = (new Email($preset, $props))->toArray(); - - return ($this->component('email'))($this, $props, $debug); - } - - /** - * Returns the environment object with access - * to the detected host, base url and dedicated options - * - * @return \Kirby\Cms\Environment - */ - public function environment() - { - return $this->environment; - } - - /** - * Finds any file in the content directory - * - * @param string $path - * @param mixed $parent - * @param bool $drafts - * @return \Kirby\Cms\File|null - */ - public function file(string $path, $parent = null, bool $drafts = true) - { - $parent = $parent ?? $this->site(); - $id = dirname($path); - $filename = basename($path); - - if (is_a($parent, 'Kirby\Cms\User') === true) { - return $parent->file($filename); - } - - if (is_a($parent, 'Kirby\Cms\File') === true) { - $parent = $parent->parent(); - } - - if ($id === '.') { - if ($file = $parent->file($filename)) { - return $file; - } elseif ($file = $this->site()->file($filename)) { - return $file; - } else { - return null; - } - } - - if ($page = $this->page($id, $parent, $drafts)) { - return $page->file($filename); - } - - if ($page = $this->page($id, null, $drafts)) { - return $page->file($filename); - } - - return null; - } - - /** - * Returns the current App instance - * - * @param \Kirby\Cms\App|null $instance - * @param bool $lazy If `true`, the instance is only returned if already existing - * @return static|null - */ - public static function instance(self $instance = null, bool $lazy = false) - { - if ($instance === null) { - if ($lazy === true) { - return static::$instance; - } else { - return static::$instance ?? new static(); - } - } - - return static::$instance = $instance; - } - - /** - * Takes almost any kind of input and - * tries to convert it into a valid response - * - * @internal - * @param mixed $input - * @return \Kirby\Http\Response - */ - public function io($input) - { - // use the current response configuration - $response = $this->response(); - - // any direct exception will be turned into an error page - if (is_a($input, 'Throwable') === true) { - if (is_a($input, 'Kirby\Exception\Exception') === true) { - $code = $input->getHttpCode(); - } else { - $code = $input->getCode(); - } - $message = $input->getMessage(); - - if ($code < 400 || $code > 599) { - $code = 500; - } - - if ($errorPage = $this->site()->errorPage()) { - return $response->code($code)->send($errorPage->render([ - 'errorCode' => $code, - 'errorMessage' => $message, - 'errorType' => get_class($input) - ])); - } - - return $response - ->code($code) - ->type('text/html') - ->send($message); - } - - // Empty input - if (empty($input) === true) { - return $this->io(new NotFoundException()); - } - - // Response Configuration - if (is_a($input, 'Kirby\Cms\Responder') === true) { - return $input->send(); - } - - // Responses - if (is_a($input, 'Kirby\Http\Response') === true) { - return $input; - } - - // Pages - if (is_a($input, 'Kirby\Cms\Page')) { - try { - $html = $input->render(); - } catch (ErrorPageException $e) { - return $this->io($e); - } - - if ($input->isErrorPage() === true) { - if ($response->code() === null) { - $response->code(404); - } - } - - return $response->send($html); - } - - // Files - if (is_a($input, 'Kirby\Cms\File')) { - return $response->redirect($input->mediaUrl(), 307)->send(); - } - - // Simple HTML response - if (is_string($input) === true) { - return $response->send($input); - } - - // array to json conversion - if (is_array($input) === true) { - return $response->json($input)->send(); - } - - throw new InvalidArgumentException('Unexpected input'); - } - - /** - * Renders a single KirbyTag with the given attributes - * - * @internal - * @param string $type - * @param string|null $value - * @param array $attr - * @param array $data - * @return string - */ - public function kirbytag(string $type, string $value = null, array $attr = [], array $data = []): string - { - $data['kirby'] = $data['kirby'] ?? $this; - $data['site'] = $data['site'] ?? $data['kirby']->site(); - $data['parent'] = $data['parent'] ?? $data['site']->page(); - - return (new KirbyTag($type, $value, $attr, $data, $this->options))->render(); - } - - /** - * KirbyTags Parser - * - * @internal - * @param string|null $text - * @param array $data - * @return string - */ - public function kirbytags(string $text = null, array $data = []): string - { - $data['kirby'] ??= $this; - $data['site'] ??= $data['kirby']->site(); - $data['parent'] ??= $data['site']->page(); - - $options = $this->options; - - $text = $this->apply('kirbytags:before', compact('text', 'data', 'options'), 'text'); - $text = KirbyTags::parse($text, $data, $options); - $text = $this->apply('kirbytags:after', compact('text', 'data', 'options'), 'text'); - - return $text; - } - - /** - * Parses KirbyTags first and Markdown afterwards - * - * @internal - * @param string|null $text - * @param array $data - * @param bool $inline (deprecated: use $data['markdown']['inline'] instead) - * @return string - * @todo add deprecation warning for $inline parameter in 3.7.0 - * @todo rename $data parameter to $options in 3.7.0 - * @todo remove $inline parameter in in 3.8.0 - */ - public function kirbytext(string $text = null, array $data = [], bool $inline = false): string - { - $options = A::merge([ - 'markdown' => [ - 'inline' => $inline - ] - ], $data); - - $text = $this->apply('kirbytext:before', compact('text'), 'text'); - $text = $this->kirbytags($text, $options); - $text = $this->markdown($text, $options['markdown']); - - if ($this->option('smartypants', false) !== false) { - $text = $this->smartypants($text); - } - - $text = $this->apply('kirbytext:after', compact('text'), 'text'); - - return $text; - } - - /** - * Returns the current language - * - * @param string|null $code - * @return \Kirby\Cms\Language|null - */ - public function language(string $code = null) - { - if ($this->multilang() === false) { - return null; - } - - if ($code === 'default') { - return $this->languages()->default(); - } - - if ($code !== null) { - return $this->languages()->find($code); - } - - return $this->language = $this->language ?? $this->languages()->default(); - } - - /** - * Returns the current language code - * - * @internal - * @param string|null $languageCode - * @return string|null - */ - public function languageCode(string $languageCode = null): ?string - { - if ($language = $this->language($languageCode)) { - return $language->code(); - } - - return null; - } - - /** - * Returns all available site languages - * - * @param bool - * @return \Kirby\Cms\Languages - */ - public function languages(bool $clone = true) - { - if ($this->languages !== null) { - return $clone === true ? clone $this->languages : $this->languages; - } - - return $this->languages = Languages::load(); - } - - /** - * Access Kirby's part loader - * - * @return \Kirby\Cms\Loader - */ - public function load() - { - return new Loader($this); - } - - /** - * Returns the app's locks object - * - * @return \Kirby\Cms\ContentLocks - */ - public function locks(): ContentLocks - { - if ($this->locks !== null) { - return $this->locks; - } - - return $this->locks = new ContentLocks(); - } - - /** - * Parses Markdown - * - * @internal - * @param string|null $text - * @param bool|array $options - * @return string - * @todo rename $inline parameter to $options in 3.7.0 - * @todo add deprecation warning for boolean $options in 3.7.0 - * @todo remove boolean $options in in 3.8.0 - */ - public function markdown(string $text = null, $inline = null): string - { - // TODO: remove after renaming parameter - $options = $inline; - - // support for the old syntax to enable inline mode as second argument - if (is_bool($options) === true) { - $options = [ - 'inline' => $options - ]; - } - - // merge global options with local options - $options = array_merge( - $this->options['markdown'] ?? [], - (array)$options - ); - - // TODO: deprecate passing the $inline parameter in 3.7.0 - // TODO: remove passing the $inline parameter in 3.8.0 - $inline = $options['inline'] ?? false; - return ($this->component('markdown'))($this, $text, $options, $inline); - } - - /** - * Check for a multilang setup - * - * @return bool - */ - public function multilang(): bool - { - if ($this->multilang !== null) { - return $this->multilang; - } - - return $this->multilang = $this->languages()->count() !== 0; - } - - /** - * Returns the nonce, which is used - * in the panel for inline scripts - * @since 3.3.0 - * - * @return string - */ - public function nonce(): string - { - return $this->nonce = $this->nonce ?? base64_encode(random_bytes(20)); - } - - /** - * Load a specific configuration option - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function option(string $key, $default = null) - { - return A::get($this->options, $key, $default); - } - - /** - * Returns all configuration options - * - * @return array - */ - public function options(): array - { - return $this->options; - } - - /** - * Load all options from files in site/config - * - * @return array - */ - protected function optionsFromConfig(): array - { - // create an empty config container - Config::$data = []; - - // load the main config options - $root = $this->root('config'); - $options = F::load($root . '/config.php', []); - - // merge into one clean options array - return $this->options = array_replace_recursive(Config::$data, $options); - } - - /** - * Load all options for the current - * server environment - * - * @return array - */ - protected function optionsFromEnvironment(): array - { - // create the environment based on the URL setup - $this->environment = new Environment($this->root('config'), $this->options['url'] ?? null); - - // merge into one clean options array - return $this->options = array_replace_recursive($this->options, $this->environment->options()); - } - - /** - * Inject options from Kirby instance props - * - * @param array $options - * @return array - */ - protected function optionsFromProps(array $options = []): array - { - return $this->options = array_replace_recursive( - $this->options, - $options - ); - } - - /** - * Merge last-minute options from ready callback - * - * @return array - */ - protected function optionsFromReadyCallback(): array - { - if (isset($this->options['ready']) === true && is_callable($this->options['ready']) === true) { - // fetch last-minute options from the callback - $options = (array)$this->options['ready']($this); - - // inject all last-minute options recursively - $this->options = array_replace_recursive($this->options, $options); - - // update the system with changed options - if ( - isset($options['debug']) === true || - isset($options['whoops']) === true || - isset($options['editor']) === true - ) { - $this->handleErrors(); - } - - if (isset($options['debug']) === true) { - $this->api = null; - } - - if (isset($options['home']) === true || isset($options['error']) === true) { - $this->site = null; - } - - // checks custom language definition for slugs - if ($slugsOption = $this->option('slugs')) { - // slugs option must be set to string or "slugs" => ["language" => "de"] as array - if (is_string($slugsOption) === true || isset($slugsOption['language']) === true) { - $this->i18n(); - } - } - } - - return $this->options; - } - - /** - * Returns any page from the content folder - * - * @param string|null $id - * @param \Kirby\Cms\Page|\Kirby\Cms\Site|null $parent - * @param bool $drafts - * @return \Kirby\Cms\Page|null - */ - public function page(?string $id = null, $parent = null, bool $drafts = true) - { - if ($id === null) { - return null; - } - - $parent = $parent ?? $this->site(); - - if ($page = $parent->find($id)) { - /** - * We passed a single $id, we can be sure that the result is - * @var \Kirby\Cms\Page $page - */ - return $page; - } - - if ($drafts === true && $draft = $parent->draft($id)) { - return $draft; - } - - return null; - } - - /** - * Returns the request path - * - * @return string - */ - public function path(): string - { - if (is_string($this->path) === true) { - return $this->path; - } - - $requestUri = '/' . $this->request()->url()->path(); - $scriptName = $_SERVER['SCRIPT_NAME']; - $scriptFile = basename($scriptName); - $scriptDir = dirname($scriptName); - $scriptPath = $scriptFile === 'index.php' ? $scriptDir : $scriptName; - $requestPath = preg_replace('!^' . preg_quote($scriptPath) . '!', '', $requestUri); - - return $this->setPath($requestPath)->path; - } - - /** - * Returns the Response object for the - * current request - * - * @param string|null $path - * @param string|null $method - * @return \Kirby\Http\Response - */ - public function render(string $path = null, string $method = null) - { - return $this->io($this->call($path, $method)); - } - - /** - * Returns the Request singleton - * - * @return \Kirby\Http\Request - */ - public function request() - { - return $this->request = $this->request ?? new Request(); - } - - /** - * Path resolver for the router - * - * @internal - * @param string|null $path - * @param string|null $language - * @return mixed - * @throws \Kirby\Exception\NotFoundException if the home page cannot be found - */ - public function resolve(string $path = null, string $language = null) - { - // set the current translation - $this->setCurrentTranslation($language); - - // set the current locale - $this->setCurrentLanguage($language); - - // the site is needed a couple times here - $site = $this->site(); - - // use the home page - if ($path === null) { - if ($homePage = $site->homePage()) { - return $homePage; - } - - throw new NotFoundException('The home page does not exist'); - } - - // search for the page by path - $page = $site->find($path); - - // search for a draft if the page cannot be found - if (!$page && $draft = $site->draft($path)) { - if ($this->user() || $draft->isVerified(get('token'))) { - $page = $draft; - } - } - - // try to resolve content representations if the path has an extension - $extension = F::extension($path); - - // no content representation? then return the page - if (empty($extension) === true) { - return $page; - } - - // only try to return a representation - // when the page has been found - if ($page) { - try { - $response = $this->response(); - $output = $page->render([], $extension); - - // attach a MIME type based on the representation - // only if no custom MIME type was set - if ($response->type() === null) { - $response->type($extension); - } - - return $response->body($output); - } catch (NotFoundException $e) { - return null; - } - } - - $id = dirname($path); - $filename = basename($path); - - // try to resolve image urls for pages and drafts - if ($page = $site->findPageOrDraft($id)) { - return $page->file($filename); - } - - // try to resolve site files at least - return $site->file($filename); - } - - /** - * Response configuration - * - * @return \Kirby\Cms\Responder - */ - public function response() - { - return $this->response = $this->response ?? new Responder(); - } - - /** - * Returns all user roles - * - * @return \Kirby\Cms\Roles - */ - public function roles() - { - return $this->roles = $this->roles ?? Roles::load($this->root('roles')); - } - - /** - * Returns a system root - * - * @param string $type - * @return string - */ - public function root(string $type = 'index'): string - { - return $this->roots->__get($type); - } - - /** - * Returns the directory structure - * - * @return \Kirby\Cms\Ingredients - */ - public function roots() - { - return $this->roots; - } - - /** - * Returns the currently active route - * - * @return \Kirby\Http\Route|null - */ - public function route() - { - return $this->router()->route(); - } - - /** - * Returns the Router singleton - * - * @internal - * @return \Kirby\Http\Router - */ - public function router() - { - $routes = $this->routes(); - - if ($this->multilang() === true) { - foreach ($routes as $index => $route) { - if (empty($route['language']) === false) { - unset($routes[$index]); - } - } - } - - return $this->router = $this->router ?? new Router($routes); - } - - /** - * Returns all defined routes - * - * @internal - * @return array - */ - public function routes(): array - { - if (is_array($this->routes) === true) { - return $this->routes; - } - - $registry = $this->extensions('routes'); - $system = $this->core->routes(); - $routes = array_merge($system['before'], $registry, $system['after']); - - return $this->routes = $routes; - } - - /** - * Returns the current session object - * - * @param array $options Additional options, see the session component - * @return \Kirby\Session\Session - */ - public function session(array $options = []) - { - // never cache responses that depend on the session - $this->response()->cache(false); - $this->response()->header('Cache-Control', 'no-store', true); - - return $this->sessionHandler()->get($options); - } - - /** - * Returns the session handler - * - * @return \Kirby\Session\AutoSession - */ - public function sessionHandler() - { - $this->sessionHandler = $this->sessionHandler ?? new AutoSession($this->root('sessions'), $this->option('session', [])); - return $this->sessionHandler; - } - - /** - * Create your own set of languages - * - * @param array|null $languages - * @return $this - */ - protected function setLanguages(array $languages = null) - { - if ($languages !== null) { - $objects = []; - - foreach ($languages as $props) { - $objects[] = new Language($props); - } - - $this->languages = new Languages($objects); - } - - return $this; - } - - /** - * Sets the request path that is - * used for the router - * - * @param string|null $path - * @return $this - */ - protected function setPath(string $path = null) - { - $this->path = $path !== null ? trim($path, '/') : null; - return $this; - } - - /** - * Sets the request - * - * @param array|null $request - * @return $this - */ - protected function setRequest(array $request = null) - { - if ($request !== null) { - $this->request = new Request($request); - } - - return $this; - } - - /** - * Create your own set of roles - * - * @param array|null $roles - * @return $this - */ - protected function setRoles(array $roles = null) - { - if ($roles !== null) { - $this->roles = Roles::factory($roles, [ - 'kirby' => $this - ]); - } - - return $this; - } - - /** - * Sets a custom Site object - * - * @param \Kirby\Cms\Site|array|null $site - * @return $this - */ - protected function setSite($site = null) - { - if (is_array($site) === true) { - $site = new Site($site + [ - 'kirby' => $this - ]); - } - - $this->site = $site; - return $this; - } - - /** - * Returns the Server object - * - * @return \Kirby\Http\Server - */ - public function server() - { - return $this->server ??= new Server(); - } - - /** - * Initializes and returns the Site object - * - * @return \Kirby\Cms\Site - */ - public function site() - { - return $this->site = $this->site ?? new Site([ - 'errorPageId' => $this->options['error'] ?? 'error', - 'homePageId' => $this->options['home'] ?? 'home', - 'kirby' => $this, - 'url' => $this->url('index'), - ]); - } - - /** - * Applies the smartypants rule on the text - * - * @internal - * @param string|null $text - * @return string - */ - public function smartypants(string $text = null): string - { - $options = $this->option('smartypants', []); - - if ($options === false) { - return $text; - } elseif (is_array($options) === false) { - $options = []; - } - - if ($this->multilang() === true) { - $languageSmartypants = $this->language()->smartypants() ?? []; - - if (empty($languageSmartypants) === false) { - $options = array_merge($options, $languageSmartypants); - } - } - - return ($this->component('smartypants'))($this, $text, $options); - } - - /** - * Uses the snippet component to create - * and return a template snippet - * - * @internal - * @param mixed $name - * @param array $data - * @return string|null - */ - public function snippet($name, array $data = []): ?string - { - return ($this->component('snippet'))($this, $name, array_merge($this->data, $data)); - } - - /** - * System check class - * - * @return \Kirby\Cms\System - */ - public function system() - { - return $this->system = $this->system ?? new System($this); - } - - /** - * Uses the template component to initialize - * and return the Template object - * - * @internal - * @return \Kirby\Cms\Template - * @param string $name - * @param string $type - * @param string $defaultType - */ - public function template(string $name, string $type = 'html', string $defaultType = 'html') - { - return ($this->component('template'))($this, $name, $type, $defaultType); - } - - /** - * Thumbnail creator - * - * @param string $src - * @param string $dst - * @param array $options - * @return string - */ - public function thumb(string $src, string $dst, array $options = []): string - { - return ($this->component('thumb'))($this, $src, $dst, $options); - } - - /** - * Trigger a hook by name - * - * @internal - * @param string $name Full event name - * @param array $args Associative array of named event arguments - * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) - * @return void - */ - public function trigger(string $name, array $args = [], ?Event $originalEvent = null) - { - $event = $originalEvent ?? new Event($name, $args); - - if ($functions = $this->extension('hooks', $name)) { - static $level = 0; - static $triggered = []; - $level++; - - foreach ($functions as $index => $function) { - if (in_array($function, $triggered[$name] ?? []) === true) { - continue; - } - - // mark the hook as triggered, to avoid endless loops - $triggered[$name][] = $function; - - // bind the App object to the hook - $event->call($this, $function); - } - - $level--; - - if ($level === 0) { - $triggered = []; - } - } - - // trigger wildcard hooks if available - $nameWildcards = $event->nameWildcards(); - if ($originalEvent === null && count($nameWildcards) > 0) { - foreach ($nameWildcards as $nameWildcard) { - $this->trigger($nameWildcard, $args, $event); - } - } - } - - /** - * Returns a system url - * - * @param string $type - * @param bool $object If set to `true`, the URL is converted to an object - * @return string|\Kirby\Http\Uri - */ - public function url(string $type = 'index', bool $object = false) - { - $url = $this->urls->__get($type); - - if ($object === true) { - if (Url::isAbsolute($url)) { - return Url::toObject($url); - } - - // index URL was configured without host, use the current host - return Uri::current([ - 'path' => $url, - 'query' => null - ]); - } - - return $url; - } - - /** - * Returns the url structure - * - * @return \Kirby\Cms\Ingredients - */ - public function urls() - { - return $this->urls; - } - - /** - * Returns the current version number from - * the composer.json (Keep that up to date! :)) - * - * @return string|null - * @throws \Kirby\Exception\LogicException if the Kirby version cannot be detected - */ - public static function version(): ?string - { - try { - return static::$version = static::$version ?? Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null; - } catch (Throwable $e) { - throw new LogicException('The Kirby version cannot be detected. The composer.json is probably missing or not readable.'); - } - } - - /** - * Creates a hash of the version number - * - * @return string - */ - public static function versionHash(): string - { - return md5(static::version()); - } - - /** - * Returns the visitor object - * - * @return \Kirby\Http\Visitor - */ - public function visitor() - { - return $this->visitor = $this->visitor ?? new Visitor(); - } + use AppCaches; + use AppErrors; + use AppPlugins; + use AppTranslations; + use AppUsers; + use Properties; + + public const CLASS_ALIAS = 'kirby'; + + protected static $instance; + protected static $version; + + public $data = []; + + protected $api; + protected $collections; + protected $core; + protected $defaultLanguage; + protected $environment; + protected $language; + protected $languages; + protected $locks; + protected $multilang; + protected $nonce; + protected $options; + protected $path; + protected $request; + protected $response; + protected $roles; + protected $roots; + protected $routes; + protected $router; + protected $sessionHandler; + protected $site; + protected $system; + protected $urls; + protected $user; + protected $users; + protected $visitor; + + /** + * Creates a new App instance + * + * @param array $props + * @param bool $setInstance If false, the instance won't be set globally + */ + public function __construct(array $props = [], bool $setInstance = true) + { + $this->core = new Core($this); + + // register all roots to be able to load stuff afterwards + $this->bakeRoots($props['roots'] ?? []); + + try { + // stuff from config and additional options + $this->optionsFromConfig(); + $this->optionsFromProps($props['options'] ?? []); + $this->optionsFromEnvironment($props); + } finally { + // register the Whoops error handler inside of a + // try-finally block to ensure it's still registered + // even if there is a problem loading the configurations + $this->handleErrors(); + } + + // a custom request setup must come before defining the path + $this->setRequest($props['request'] ?? null); + + // set the path to make it available for the url bakery + $this->setPath($props['path'] ?? null); + + // create all urls after the config, so possible + // options can be taken into account + $this->bakeUrls($props['urls'] ?? []); + + // configurable properties + $this->setOptionalProperties($props, [ + 'languages', + 'roles', + 'site', + 'user', + 'users' + ]); + + // set the singleton + if (static::$instance === null || $setInstance === true) { + Model::$kirby = static::$instance = $this; + } + + // setup the I18n class with the translation loader + $this->i18n(); + + // load all extensions + $this->extensionsFromSystem(); + $this->extensionsFromProps($props); + $this->extensionsFromPlugins(); + $this->extensionsFromOptions(); + $this->extensionsFromFolders(); + + // trigger hook for use in plugins + $this->trigger('system.loadPlugins:after'); + + // execute a ready callback from the config + $this->optionsFromReadyCallback(); + + // bake config + $this->bakeOptions(); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'languages' => $this->languages(), + 'options' => $this->options(), + 'request' => $this->request(), + 'roots' => $this->roots(), + 'site' => $this->site(), + 'urls' => $this->urls(), + 'version' => $this->version(), + ]; + } + + /** + * Returns the Api instance + * + * @internal + * @return \Kirby\Cms\Api + */ + public function api() + { + if ($this->api !== null) { + return $this->api; + } + + $root = $this->root('kirby') . '/config/api'; + $extensions = $this->extensions['api'] ?? []; + $routes = (include $root . '/routes.php')($this); + + $api = [ + 'debug' => $this->option('debug', false), + 'authentication' => $extensions['authentication'] ?? include $root . '/authentication.php', + 'data' => $extensions['data'] ?? [], + 'collections' => array_merge($extensions['collections'] ?? [], include $root . '/collections.php'), + 'models' => array_merge($extensions['models'] ?? [], include $root . '/models.php'), + 'routes' => array_merge($routes, $extensions['routes'] ?? []), + 'kirby' => $this, + ]; + + return $this->api = new Api($api); + } + + /** + * Applies a hook to the given value + * + * @internal + * @param string $name Full event name + * @param array $args Associative array of named event arguments + * @param string $modify Key in $args that is modified by the hooks + * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) + * @return mixed Resulting value as modified by the hooks + */ + public function apply(string $name, array $args, string $modify, ?Event $originalEvent = null) + { + $event = $originalEvent ?? new Event($name, $args); + + if ($functions = $this->extension('hooks', $name)) { + foreach ($functions as $function) { + // bind the App object to the hook + $newValue = $event->call($this, $function); + + // update value if one was returned + if ($newValue !== null) { + $event->updateArgument($modify, $newValue); + } + } + } + + // apply wildcard hooks if available + $nameWildcards = $event->nameWildcards(); + if ($originalEvent === null && count($nameWildcards) > 0) { + foreach ($nameWildcards as $nameWildcard) { + // the $event object is passed by reference + // and will be modified down the chain + $this->apply($nameWildcard, $event->arguments(), $modify, $event); + } + } + + return $event->argument($modify); + } + + /** + * Normalizes and globally sets the configured options + * + * @return $this + */ + protected function bakeOptions() + { + // convert the old plugin option syntax to the new one + foreach ($this->options as $key => $value) { + // detect option keys with the `vendor.plugin.option` format + if (preg_match('/^([a-z0-9-]+\.[a-z0-9-]+)\.(.*)$/i', $key, $matches) === 1) { + list(, $plugin, $option) = $matches; + + // verify that it's really a plugin option + if (isset(static::$plugins[str_replace('.', '/', $plugin)]) !== true) { + continue; + } + + // ensure that the target option array exists + // (which it will if the plugin has any options) + if (isset($this->options[$plugin]) !== true) { + $this->options[$plugin] = []; // @codeCoverageIgnore + } + + // move the option to the plugin option array + // don't overwrite nested arrays completely but merge them + $this->options[$plugin] = array_replace_recursive($this->options[$plugin], [$option => $value]); + unset($this->options[$key]); + } + } + + Config::$data = $this->options; + return $this; + } + + /** + * Sets the directory structure + * + * @param array|null $roots + * @return $this + */ + protected function bakeRoots(array $roots = null) + { + $roots = array_merge($this->core->roots(), (array)$roots); + $this->roots = Ingredients::bake($roots); + return $this; + } + + /** + * Sets the Url structure + * + * @param array|null $urls + * @return $this + */ + protected function bakeUrls(array $urls = null) + { + $urls = array_merge($this->core->urls(), (array)$urls); + $this->urls = Ingredients::bake($urls); + return $this; + } + + /** + * Returns all available blueprints for this installation + * + * @param string $type + * @return array + */ + public function blueprints(string $type = 'pages'): array + { + $blueprints = []; + + foreach ($this->extensions('blueprints') as $name => $blueprint) { + if (dirname($name) === $type) { + $name = basename($name); + $blueprints[$name] = $name; + } + } + + foreach (glob($this->root('blueprints') . '/' . $type . '/*.yml') as $blueprint) { + $name = F::name($blueprint); + $blueprints[$name] = $name; + } + + ksort($blueprints); + + return array_values($blueprints); + } + + /** + * Calls any Kirby route + * + * @param string|null $path + * @param string|null $method + * @return mixed + */ + public function call(string $path = null, string $method = null) + { + $path ??= $this->path(); + $method ??= $this->request()->method(); + return $this->router()->call($path, $method); + } + + /** + * Creates an instance with the same + * initial properties + * + * @param array $props + * @param bool $setInstance If false, the instance won't be set globally + * @return static + */ + public function clone(array $props = [], bool $setInstance = true) + { + $props = array_replace_recursive($this->propertyData, $props); + + $clone = new static($props, $setInstance); + $clone->data = $this->data; + + return $clone; + } + + /** + * Returns a specific user-defined collection + * by name. All relevant dependencies are + * automatically injected + * + * @param string $name + * @return \Kirby\Cms\Collection|null + */ + public function collection(string $name) + { + return $this->collections()->get($name, [ + 'kirby' => $this, + 'site' => $this->site(), + 'pages' => $this->site()->children(), + 'users' => $this->users() + ]); + } + + /** + * Returns all user-defined collections + * + * @return \Kirby\Cms\Collections + */ + public function collections() + { + return $this->collections = $this->collections ?? new Collections(); + } + + /** + * Returns a core component + * + * @internal + * @param string $name + * @return mixed + */ + public function component($name) + { + return $this->extensions['components'][$name] ?? null; + } + + /** + * Returns the content extension + * + * @internal + * @return string + */ + public function contentExtension(): string + { + return $this->options['content']['extension'] ?? 'txt'; + } + + /** + * Returns files that should be ignored when scanning folders + * + * @internal + * @return array + */ + public function contentIgnore(): array + { + return $this->options['content']['ignore'] ?? Dir::$ignore; + } + + /** + * Generates a non-guessable token based on model + * data and a configured salt + * + * @param mixed $model Object to pass to the salt callback if configured + * @param string $value Model data to include in the generated token + * @return string + */ + public function contentToken($model, string $value): string + { + if (method_exists($model, 'root') === true) { + $default = $model->root(); + } else { + $default = $this->root('content'); + } + + $salt = $this->option('content.salt', $default); + + if (is_a($salt, 'Closure') === true) { + $salt = $salt($model); + } + + return hash_hmac('sha1', $value, $salt); + } + + /** + * Calls a page controller by name + * and with the given arguments + * + * @internal + * @param string $name + * @param array $arguments + * @param string $contentType + * @return array + */ + public function controller(string $name, array $arguments = [], string $contentType = 'html'): array + { + $name = basename(strtolower($name)); + + if ($controller = $this->controllerLookup($name, $contentType)) { + return (array)$controller->call($this, $arguments); + } + + if ($contentType !== 'html') { + // no luck for a specific representation controller? + // let's try the html controller instead + if ($controller = $this->controllerLookup($name)) { + return (array)$controller->call($this, $arguments); + } + } + + // still no luck? Let's take the site controller + if ($controller = $this->controllerLookup('site')) { + return (array)$controller->call($this, $arguments); + } + + return []; + } + + /** + * Try to find a controller by name + * + * @param string $name + * @param string $contentType + * @return \Kirby\Toolkit\Controller|null + */ + protected function controllerLookup(string $name, string $contentType = 'html') + { + if ($contentType !== null && $contentType !== 'html') { + $name .= '.' . $contentType; + } + + // controller on disk + if ($controller = Controller::load($this->root('controllers') . '/' . $name . '.php')) { + return $controller; + } + + // registry controller + if ($controller = $this->extension('controllers', $name)) { + return is_a($controller, 'Kirby\Toolkit\Controller') ? $controller : new Controller($controller); + } + + return null; + } + + /** + * Get access to object that lists + * all parts of Kirby core + * + * @return \Kirby\Cms\Core + */ + public function core() + { + return $this->core; + } + + /** + * Checks/returns a CSRF token + * @since 3.7.0 + * + * @param string|null $check Pass a token here to compare it to the one in the session + * @return string|bool Either the token or a boolean check result + */ + public function csrf(?string $check = null) + { + $session = $this->session(); + + // no arguments, generate/return a token + // (check explicitly if there have been no arguments at all; + // checking for null introduces a security issue because null could come + // from user input or bugs in the calling code!) + if (func_num_args() === 0) { + $token = $session->get('kirby.csrf'); + + if (is_string($token) !== true) { + $token = bin2hex(random_bytes(32)); + $session->set('kirby.csrf', $token); + } + + return $token; + } + + // argument has been passed, check the token + if ( + is_string($check) === true && + is_string($session->get('kirby.csrf')) === true + ) { + return hash_equals($session->get('kirby.csrf'), $check) === true; + } + + return false; + } + + /** + * Returns the default language object + * + * @return \Kirby\Cms\Language|null + */ + public function defaultLanguage() + { + return $this->defaultLanguage = $this->defaultLanguage ?? $this->languages()->default(); + } + + /** + * Destroy the instance singleton and + * purge other static props + * + * @internal + */ + public static function destroy(): void + { + static::$plugins = []; + static::$instance = null; + } + + /** + * Detect the preferred language from the visitor object + * + * @return \Kirby\Cms\Language + */ + public function detectedLanguage() + { + $languages = $this->languages(); + $visitor = $this->visitor(); + + foreach ($visitor->acceptedLanguages() as $lang) { + if ($language = $languages->findBy('locale', $lang->locale(LC_ALL))) { + return $language; + } + } + + foreach ($visitor->acceptedLanguages() as $lang) { + if ($language = $languages->findBy('code', $lang->code())) { + return $language; + } + } + + return $this->defaultLanguage(); + } + + /** + * Returns the Email singleton + * + * @param mixed $preset + * @param array $props + * @return \Kirby\Email\Email + */ + public function email($preset = [], array $props = []) + { + $debug = $props['debug'] ?? false; + $props = (new Email($preset, $props))->toArray(); + + return ($this->component('email'))($this, $props, $debug); + } + + /** + * Returns the environment object with access + * to the detected host, base url and dedicated options + * + * @return \Kirby\Http\Environment + */ + public function environment() + { + return $this->environment ?? new Environment(); + } + + /** + * Finds any file in the content directory + * + * @param string $path + * @param mixed $parent + * @param bool $drafts + * @return \Kirby\Cms\File|null + */ + public function file(string $path, $parent = null, bool $drafts = true) + { + $parent = $parent ?? $this->site(); + $id = dirname($path); + $filename = basename($path); + + if (is_a($parent, 'Kirby\Cms\User') === true) { + return $parent->file($filename); + } + + if (is_a($parent, 'Kirby\Cms\File') === true) { + $parent = $parent->parent(); + } + + if ($id === '.') { + if ($file = $parent->file($filename)) { + return $file; + } + + if ($file = $this->site()->file($filename)) { + return $file; + } + + return null; + } + + if ($page = $this->page($id, $parent, $drafts)) { + return $page->file($filename); + } + + if ($page = $this->page($id, null, $drafts)) { + return $page->file($filename); + } + + return null; + } + + /** + * Return an image from any page + * specified by the path + * + * Example: + * + * + * @param string|null $path + * @return \Kirby\Cms\File|null + * + * @todo merge with App::file() + */ + public function image(?string $path = null) + { + if ($path === null) { + return $this->site()->page()->image(); + } + + $uri = dirname($path); + $filename = basename($path); + + if ($uri === '.') { + $uri = null; + } + + switch ($uri) { + case '/': + $parent = $this->site(); + break; + case null: + $parent = $this->site()->page(); + break; + default: + $parent = $this->site()->page($uri); + break; + } + + if ($parent) { + return $parent->image($filename); + } + + return null; + } + + /** + * Returns the current App instance + * + * @param \Kirby\Cms\App|null $instance + * @param bool $lazy If `true`, the instance is only returned if already existing + * @return static|null + */ + public static function instance(self $instance = null, bool $lazy = false) + { + if ($instance === null) { + if ($lazy === true) { + return static::$instance; + } else { + return static::$instance ?? new static(); + } + } + + return static::$instance = $instance; + } + + /** + * Takes almost any kind of input and + * tries to convert it into a valid response + * + * @internal + * @param mixed $input + * @return \Kirby\Http\Response + */ + public function io($input) + { + // use the current response configuration + $response = $this->response(); + + // any direct exception will be turned into an error page + if (is_a($input, 'Throwable') === true) { + if (is_a($input, 'Kirby\Exception\Exception') === true) { + $code = $input->getHttpCode(); + } else { + $code = $input->getCode(); + } + $message = $input->getMessage(); + + if ($code < 400 || $code > 599) { + $code = 500; + } + + if ($errorPage = $this->site()->errorPage()) { + return $response->code($code)->send($errorPage->render([ + 'errorCode' => $code, + 'errorMessage' => $message, + 'errorType' => get_class($input) + ])); + } + + return $response + ->code($code) + ->type('text/html') + ->send($message); + } + + // Empty input + if (empty($input) === true) { + return $this->io(new NotFoundException()); + } + + // (Modified) global response configuration, e.g. in routes + if (is_a($input, 'Kirby\Cms\Responder') === true) { + // return the passed object unmodified (without injecting headers + // from the global object) to allow a complete response override + // https://github.com/getkirby/kirby/pull/4144#issuecomment-1034766726 + return $input->send(); + } + + // Responses + if (is_a($input, 'Kirby\Http\Response') === true) { + $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'] = array_merge($response->headers(), $data['headers']); + + return new Response($data); + } + + // Pages + if (is_a($input, 'Kirby\Cms\Page')) { + try { + $html = $input->render(); + } catch (ErrorPageException $e) { + return $this->io($e); + } + + if ($input->isErrorPage() === true) { + if ($response->code() === null) { + $response->code(404); + } + } + + return $response->send($html); + } + + // Files + if (is_a($input, 'Kirby\Cms\File')) { + return $response->redirect($input->mediaUrl(), 307)->send(); + } + + // Simple HTML response + if (is_string($input) === true) { + return $response->send($input); + } + + // array to json conversion + if (is_array($input) === true) { + return $response->json($input)->send(); + } + + throw new InvalidArgumentException('Unexpected input'); + } + + /** + * Renders a single KirbyTag with the given attributes + * + * @internal + * @param string|array $type Tag type or array with all tag arguments + * (the key of the first element becomes the type) + * @param string|null $value + * @param array $attr + * @param array $data + * @return string + */ + public function kirbytag($type, ?string $value = null, array $attr = [], array $data = []): string + { + if (is_array($type) === true) { + $kirbytag = $type; + $type = key($kirbytag); + $value = current($kirbytag); + $attr = $kirbytag; + + // check data attribute and separate from attr data if exists + if (isset($attr['data']) === true) { + $data = $attr['data']; + unset($attr['data']); + } + } + + $data['kirby'] = $data['kirby'] ?? $this; + $data['site'] = $data['site'] ?? $data['kirby']->site(); + $data['parent'] = $data['parent'] ?? $data['site']->page(); + + return (new KirbyTag($type, $value, $attr, $data, $this->options))->render(); + } + + /** + * KirbyTags Parser + * + * @internal + * @param string|null $text + * @param array $data + * @return string + */ + public function kirbytags(string $text = null, array $data = []): string + { + $data['kirby'] ??= $this; + $data['site'] ??= $data['kirby']->site(); + $data['parent'] ??= $data['site']->page(); + + $options = $this->options; + + $text = $this->apply('kirbytags:before', compact('text', 'data', 'options'), 'text'); + $text = KirbyTags::parse($text, $data, $options); + $text = $this->apply('kirbytags:after', compact('text', 'data', 'options'), 'text'); + + return $text; + } + + /** + * Parses KirbyTags first and Markdown afterwards + * + * @internal + * @param string|null $text + * @param array $options + * @param bool $inline (deprecated: use $options['markdown']['inline'] instead) + * @return string + * @todo remove $inline parameter in in 3.8.0 + */ + public function kirbytext(string $text = null, array $options = [], bool $inline = false): string + { + // warning for deprecated fourth parameter + // @codeCoverageIgnoreStart + if (func_num_args() === 3) { + Helpers::deprecated('Cms\App::kirbytext(): the $inline parameter is deprecated and will be removed in Kirby 3.8.0. Use $options[\'markdown\'][\'inline\'] instead.'); + } + // @codeCoverageIgnoreEnd + + $options['markdown']['inline'] ??= $inline; + + $text = $this->apply('kirbytext:before', compact('text'), 'text'); + $text = $this->kirbytags($text, $options); + $text = $this->markdown($text, $options['markdown']); + + if ($this->option('smartypants', false) !== false) { + $text = $this->smartypants($text); + } + + $text = $this->apply('kirbytext:after', compact('text'), 'text'); + + return $text; + } + + /** + * Returns the current language + * + * @param string|null $code + * @return \Kirby\Cms\Language|null + */ + public function language(string $code = null) + { + if ($this->multilang() === false) { + return null; + } + + if ($code === 'default') { + return $this->languages()->default(); + } + + if ($code !== null) { + return $this->languages()->find($code); + } + + return $this->language = $this->language ?? $this->languages()->default(); + } + + /** + * Returns the current language code + * + * @internal + * @param string|null $languageCode + * @return string|null + */ + public function languageCode(string $languageCode = null): ?string + { + if ($language = $this->language($languageCode)) { + return $language->code(); + } + + return null; + } + + /** + * Returns all available site languages + * + * @param bool + * @return \Kirby\Cms\Languages + */ + public function languages(bool $clone = true) + { + if ($this->languages !== null) { + return $clone === true ? clone $this->languages : $this->languages; + } + + return $this->languages = Languages::load(); + } + + /** + * Access Kirby's part loader + * + * @return \Kirby\Cms\Loader + */ + public function load() + { + return new Loader($this); + } + + /** + * Returns the app's locks object + * + * @return \Kirby\Cms\ContentLocks + */ + public function locks(): ContentLocks + { + if ($this->locks !== null) { + return $this->locks; + } + + return $this->locks = new ContentLocks(); + } + + /** + * Parses Markdown + * + * @internal + * @param string|null $text + * @param bool|array $options Boolean inline value is deprecated, use `['inline' => true]` instead + * @return string + * @todo remove boolean $options in in 3.8.0 + */ + public function markdown(string $text = null, $options = null): string + { + // support for the old syntax to enable inline mode as second argument + // @codeCoverageIgnoreStart + if (is_bool($options) === true) { + Helpers::deprecated('Cms\App::markdown(): Passing a boolean as second parameter has been deprecated and won\'t be supported anymore in Kirby 3.8.0. Instead pass array with the key "inline" set to true or false.'); + + $options = [ + 'inline' => $options + ]; + } + // @codeCoverageIgnoreEnd + + // merge global options with local options + $options = array_merge( + $this->options['markdown'] ?? [], + (array)$options + ); + + // TODO: remove passing the $inline parameter in 3.8.0 + // $options['inline'] is set to `false` to avoid the deprecation + // warning in the component; this can also be removed in 3.8.0 + $inline = $options['inline'] ??= false; + return ($this->component('markdown'))($this, $text, $options, $inline); + } + + /** + * Check for a multilang setup + * + * @return bool + */ + public function multilang(): bool + { + if ($this->multilang !== null) { + return $this->multilang; + } + + return $this->multilang = $this->languages()->count() !== 0; + } + + /** + * Returns the nonce, which is used + * in the panel for inline scripts + * @since 3.3.0 + * + * @return string + */ + public function nonce(): string + { + return $this->nonce = $this->nonce ?? base64_encode(random_bytes(20)); + } + + /** + * Load a specific configuration option + * + * @param string $key + * @param mixed $default + * @return mixed + */ + public function option(string $key, $default = null) + { + return A::get($this->options, $key, $default); + } + + /** + * Returns all configuration options + * + * @return array + */ + public function options(): array + { + return $this->options; + } + + /** + * Load all options from files in site/config + * + * @return array + */ + protected function optionsFromConfig(): array + { + // create an empty config container + Config::$data = []; + + // load the main config options + $root = $this->root('config'); + $options = F::load($root . '/config.php', []); + + // merge into one clean options array + return $this->options = array_replace_recursive(Config::$data, $options); + } + + /** + * Load all options for the current + * server environment + * + * @param array $props + * @return array + */ + protected function optionsFromEnvironment(array $props = []): array + { + $globalUrl = $this->options['url'] ?? null; + + // create the environment based on the URL setup + $this->environment = new Environment([ + 'allowed' => $globalUrl, + 'cli' => $props['cli'] ?? null, + ], $props['server'] ?? null); + + // merge into one clean options array + $options = $this->environment()->options($this->root('config')); + $this->options = array_replace_recursive($this->options, $options); + + // reload the environment if the environment config has overridden + // the `url` option; this ensures that the base URL is correct + $envUrl = $this->options['url'] ?? null; + if ($envUrl !== $globalUrl) { + $this->environment->detect([ + 'allowed' => $envUrl, + 'cli' => $props['cli'] ?? null + ], $props['server'] ?? null); + } + + return $this->options; + } + + /** + * Inject options from Kirby instance props + * + * @param array $options + * @return array + */ + protected function optionsFromProps(array $options = []): array + { + return $this->options = array_replace_recursive( + $this->options, + $options + ); + } + + /** + * Merge last-minute options from ready callback + * + * @return array + */ + protected function optionsFromReadyCallback(): array + { + if (isset($this->options['ready']) === true && is_callable($this->options['ready']) === true) { + // fetch last-minute options from the callback + $options = (array)$this->options['ready']($this); + + // inject all last-minute options recursively + $this->options = array_replace_recursive($this->options, $options); + + // update the system with changed options + if ( + isset($options['debug']) === true || + isset($options['whoops']) === true || + isset($options['editor']) === true + ) { + $this->handleErrors(); + } + + if (isset($options['debug']) === true) { + $this->api = null; + } + + if (isset($options['home']) === true || isset($options['error']) === true) { + $this->site = null; + } + + // checks custom language definition for slugs + if ($slugsOption = $this->option('slugs')) { + // slugs option must be set to string or "slugs" => ["language" => "de"] as array + if (is_string($slugsOption) === true || isset($slugsOption['language']) === true) { + $this->i18n(); + } + } + } + + return $this->options; + } + + /** + * Returns any page from the content folder + * + * @param string|null $id + * @param \Kirby\Cms\Page|\Kirby\Cms\Site|null $parent + * @param bool $drafts + * @return \Kirby\Cms\Page|null + */ + public function page(?string $id = null, $parent = null, bool $drafts = true) + { + if ($id === null) { + return null; + } + + $parent = $parent ?? $this->site(); + + if ($page = $parent->find($id)) { + /** + * We passed a single $id, we can be sure that the result is + * @var \Kirby\Cms\Page $page + */ + return $page; + } + + if ($drafts === true && $draft = $parent->draft($id)) { + return $draft; + } + + return null; + } + + /** + * Returns the request path + * + * @return string + */ + public function path(): string + { + if (is_string($this->path) === true) { + return $this->path; + } + + $current = $this->request()->path()->toString(); + $index = $this->environment()->baseUri()->path()->toString(); + $path = Str::afterStart($current, $index); + + return $this->setPath($path)->path; + } + + /** + * Returns the Response object for the + * current request + * + * @param string|null $path + * @param string|null $method + * @return \Kirby\Http\Response + */ + public function render(string $path = null, string $method = null) + { + return $this->io($this->call($path, $method)); + } + + /** + * Returns the Request singleton + * + * @return \Kirby\Http\Request + */ + public function request() + { + if ($this->request !== null) { + return $this->request; + } + + $env = $this->environment(); + + return $this->request = new Request([ + 'cli' => $env->cli(), + 'url' => $env->requestUri() + ]); + } + + /** + * Path resolver for the router + * + * @internal + * @param string|null $path + * @param string|null $language + * @return mixed + * @throws \Kirby\Exception\NotFoundException if the home page cannot be found + */ + public function resolve(string $path = null, string $language = null) + { + // set the current translation + $this->setCurrentTranslation($language); + + // set the current locale + $this->setCurrentLanguage($language); + + // the site is needed a couple times here + $site = $this->site(); + + // use the home page + if ($path === null) { + if ($homePage = $site->homePage()) { + return $homePage; + } + + throw new NotFoundException('The home page does not exist'); + } + + // search for the page by path + $page = $site->find($path); + + // search for a draft if the page cannot be found + if (!$page && $draft = $site->draft($path)) { + if ($this->user() || $draft->isVerified($this->request()->get('token'))) { + $page = $draft; + } + } + + // try to resolve content representations if the path has an extension + $extension = F::extension($path); + + // no content representation? then return the page + if (empty($extension) === true) { + return $page; + } + + // only try to return a representation + // when the page has been found + if ($page) { + try { + $response = $this->response(); + $output = $page->render([], $extension); + + // attach a MIME type based on the representation + // only if no custom MIME type was set + if ($response->type() === null) { + $response->type($extension); + } + + return $response->body($output); + } catch (NotFoundException $e) { + return null; + } + } + + $id = dirname($path); + $filename = basename($path); + + // try to resolve image urls for pages and drafts + if ($page = $site->findPageOrDraft($id)) { + return $page->file($filename); + } + + // try to resolve site files at least + return $site->file($filename); + } + + /** + * Response configuration + * + * @return \Kirby\Cms\Responder + */ + public function response() + { + return $this->response = $this->response ?? new Responder(); + } + + /** + * Returns all user roles + * + * @return \Kirby\Cms\Roles + */ + public function roles() + { + return $this->roles = $this->roles ?? Roles::load($this->root('roles')); + } + + /** + * Returns a system root + * + * @param string $type + * @return string|null + */ + public function root(string $type = 'index'): ?string + { + return $this->roots->__get($type); + } + + /** + * Returns the directory structure + * + * @return \Kirby\Cms\Ingredients + */ + public function roots() + { + return $this->roots; + } + + /** + * Returns the currently active route + * + * @return \Kirby\Http\Route|null + */ + public function route() + { + return $this->router()->route(); + } + + /** + * Returns the Router singleton + * + * @internal + * @return \Kirby\Http\Router + */ + public function router() + { + $routes = $this->routes(); + + if ($this->multilang() === true) { + foreach ($routes as $index => $route) { + if (empty($route['language']) === false) { + unset($routes[$index]); + } + } + } + + $hooks = [ + 'beforeEach' => function ($route, $path, $method) { + $this->trigger('route:before', compact('route', 'path', 'method')); + }, + 'afterEach' => function ($route, $path, $method, $result, $final) { + return $this->apply('route:after', compact('route', 'path', 'method', 'result', 'final'), 'result'); + } + ]; + + return $this->router ??= new Router($routes, $hooks); + } + + /** + * Returns all defined routes + * + * @internal + * @return array + */ + public function routes(): array + { + if (is_array($this->routes) === true) { + return $this->routes; + } + + $registry = $this->extensions('routes'); + $system = $this->core->routes(); + $routes = array_merge($system['before'], $registry, $system['after']); + + return $this->routes = $routes; + } + + /** + * Returns the current session object + * + * @param array $options Additional options, see the session component + * @return \Kirby\Session\Session + */ + public function session(array $options = []) + { + $session = $this->sessionHandler()->get($options); + + // disable caching for sessions that use the `Authorization` header; + // cookie sessions are already covered by the `Cookie` class + if ($session->mode() === 'manual') { + $this->response()->cache(false); + $this->response()->header('Cache-Control', 'no-store, private', true); + } + + return $session; + } + + /** + * Returns the session handler + * + * @return \Kirby\Session\AutoSession + */ + public function sessionHandler() + { + $this->sessionHandler = $this->sessionHandler ?? new AutoSession($this->root('sessions'), $this->option('session', [])); + return $this->sessionHandler; + } + + /** + * Create your own set of languages + * + * @param array|null $languages + * @return $this + */ + protected function setLanguages(array $languages = null) + { + if ($languages !== null) { + $objects = []; + + foreach ($languages as $props) { + $objects[] = new Language($props); + } + + $this->languages = new Languages($objects); + } + + return $this; + } + + /** + * Sets the request path that is + * used for the router + * + * @param string|null $path + * @return $this + */ + protected function setPath(string $path = null) + { + $this->path = $path !== null ? trim($path, '/') : null; + return $this; + } + + /** + * Sets the request + * + * @param array|null $request + * @return $this + */ + protected function setRequest(array $request = null) + { + if ($request !== null) { + $this->request = new Request($request); + } + + return $this; + } + + /** + * Create your own set of roles + * + * @param array|null $roles + * @return $this + */ + protected function setRoles(array $roles = null) + { + if ($roles !== null) { + $this->roles = Roles::factory($roles, [ + 'kirby' => $this + ]); + } + + return $this; + } + + /** + * Sets a custom Site object + * + * @param \Kirby\Cms\Site|array|null $site + * @return $this + */ + protected function setSite($site = null) + { + if (is_array($site) === true) { + $site = new Site($site + [ + 'kirby' => $this + ]); + } + + $this->site = $site; + return $this; + } + + /** + * Returns the Environment object + * @deprecated 3.7.0 Use `$kirby->environment()` instead + * + * @return \Kirby\Http\Environment + * @todo Start throwing deprecation warnings in 3.8.0 + * @todo Remove in 3.9.0 + * @codeCoverageIgnore + */ + public function server() + { + return $this->environment(); + } + + /** + * Initializes and returns the Site object + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->site = $this->site ?? new Site([ + 'errorPageId' => $this->options['error'] ?? 'error', + 'homePageId' => $this->options['home'] ?? 'home', + 'kirby' => $this, + 'url' => $this->url('index'), + ]); + } + + /** + * Applies the smartypants rule on the text + * + * @internal + * @param string|null $text + * @return string + */ + public function smartypants(string $text = null): string + { + $options = $this->option('smartypants', []); + + if ($options === false) { + return $text; + } elseif (is_array($options) === false) { + $options = []; + } + + if ($this->multilang() === true) { + $languageSmartypants = $this->language()->smartypants() ?? []; + + if (empty($languageSmartypants) === false) { + $options = array_merge($options, $languageSmartypants); + } + } + + return ($this->component('smartypants'))($this, $text, $options); + } + + /** + * Uses the snippet component to create + * and return a template snippet + * + * @internal + * @param mixed $name + * @param array|object $data Variables or an object that becomes `$item` + * @param bool $return On `false`, directly echo the snippet + * @return string|null + */ + public function snippet($name, $data = [], bool $return = true): ?string + { + if (is_object($data) === true) { + $data = ['item' => $data]; + } + + $snippet = ($this->component('snippet'))($this, $name, array_merge($this->data, $data)); + + if ($return === true) { + return $snippet; + } + + echo $snippet; + return null; + } + + /** + * System check class + * + * @return \Kirby\Cms\System + */ + public function system() + { + return $this->system = $this->system ?? new System($this); + } + + /** + * Uses the template component to initialize + * and return the Template object + * + * @internal + * @return \Kirby\Cms\Template + * @param string $name + * @param string $type + * @param string $defaultType + */ + public function template(string $name, string $type = 'html', string $defaultType = 'html') + { + return ($this->component('template'))($this, $name, $type, $defaultType); + } + + /** + * Thumbnail creator + * + * @param string $src + * @param string $dst + * @param array $options + * @return string + */ + public function thumb(string $src, string $dst, array $options = []): string + { + return ($this->component('thumb'))($this, $src, $dst, $options); + } + + /** + * Trigger a hook by name + * + * @internal + * @param string $name Full event name + * @param array $args Associative array of named event arguments + * @param \Kirby\Cms\Event|null $originalEvent Event object (internal use) + * @return void + */ + public function trigger(string $name, array $args = [], ?Event $originalEvent = null) + { + $event = $originalEvent ?? new Event($name, $args); + + if ($functions = $this->extension('hooks', $name)) { + static $level = 0; + static $triggered = []; + $level++; + + foreach ($functions as $index => $function) { + if (in_array($function, $triggered[$name] ?? []) === true) { + continue; + } + + // mark the hook as triggered, to avoid endless loops + $triggered[$name][] = $function; + + // bind the App object to the hook + $event->call($this, $function); + } + + $level--; + + if ($level === 0) { + $triggered = []; + } + } + + // trigger wildcard hooks if available + $nameWildcards = $event->nameWildcards(); + if ($originalEvent === null && count($nameWildcards) > 0) { + foreach ($nameWildcards as $nameWildcard) { + $this->trigger($nameWildcard, $args, $event); + } + } + } + + /** + * Returns a system url + * + * @param string $type + * @param bool $object If set to `true`, the URL is converted to an object + * @return string|\Kirby\Http\Uri|null + */ + public function url(string $type = 'index', bool $object = false) + { + $url = $this->urls->__get($type); + + if ($object === true) { + if (Url::isAbsolute($url)) { + return Url::toObject($url); + } + + // index URL was configured without host, use the current host + return Uri::current([ + 'path' => $url, + 'query' => null + ]); + } + + return $url; + } + + /** + * Returns the url structure + * + * @return \Kirby\Cms\Ingredients + */ + public function urls() + { + return $this->urls; + } + + /** + * Returns the current version number from + * the composer.json (Keep that up to date! :)) + * + * @return string|null + * @throws \Kirby\Exception\LogicException if the Kirby version cannot be detected + */ + public static function version(): ?string + { + try { + return static::$version = static::$version ?? Data::read(dirname(__DIR__, 2) . '/composer.json')['version'] ?? null; + } catch (Throwable $e) { + throw new LogicException('The Kirby version cannot be detected. The composer.json is probably missing or not readable.'); + } + } + + /** + * Creates a hash of the version number + * + * @return string + */ + public static function versionHash(): string + { + return md5(static::version()); + } + + /** + * Returns the visitor object + * + * @return \Kirby\Http\Visitor + */ + public function visitor() + { + return $this->visitor = $this->visitor ?? new Visitor(); + } } diff --git a/kirby/src/Cms/AppCaches.php b/kirby/src/Cms/AppCaches.php index c9dff45..2bea1ed 100644 --- a/kirby/src/Cms/AppCaches.php +++ b/kirby/src/Cms/AppCaches.php @@ -16,122 +16,122 @@ use Kirby\Exception\InvalidArgumentException; */ trait AppCaches { - protected $caches = []; + protected $caches = []; - /** - * Returns a cache instance by key - * - * @param string $key - * @return \Kirby\Cache\Cache - */ - public function cache(string $key) - { - if (isset($this->caches[$key]) === true) { - return $this->caches[$key]; - } + /** + * Returns a cache instance by key + * + * @param string $key + * @return \Kirby\Cache\Cache + */ + public function cache(string $key) + { + if (isset($this->caches[$key]) === true) { + return $this->caches[$key]; + } - // get the options for this cache type - $options = $this->cacheOptions($key); + // get the options for this cache type + $options = $this->cacheOptions($key); - if ($options['active'] === false) { - // use a dummy cache that does nothing - return $this->caches[$key] = new NullCache(); - } + if ($options['active'] === false) { + // use a dummy cache that does nothing + return $this->caches[$key] = new NullCache(); + } - $type = strtolower($options['type']); - $types = $this->extensions['cacheTypes'] ?? []; + $type = strtolower($options['type']); + $types = $this->extensions['cacheTypes'] ?? []; - if (array_key_exists($type, $types) === false) { - throw new InvalidArgumentException([ - 'key' => 'app.invalid.cacheType', - 'data' => ['type' => $type] - ]); - } + if (array_key_exists($type, $types) === false) { + throw new InvalidArgumentException([ + 'key' => 'app.invalid.cacheType', + 'data' => ['type' => $type] + ]); + } - $className = $types[$type]; + $className = $types[$type]; - // initialize the cache class - $cache = new $className($options); + // initialize the cache class + $cache = new $className($options); - // check if it is a usable cache object - if (is_a($cache, 'Kirby\Cache\Cache') !== true) { - throw new InvalidArgumentException([ - 'key' => 'app.invalid.cacheType', - 'data' => ['type' => $type] - ]); - } + // check if it is a usable cache object + if (is_a($cache, 'Kirby\Cache\Cache') !== true) { + throw new InvalidArgumentException([ + 'key' => 'app.invalid.cacheType', + 'data' => ['type' => $type] + ]); + } - return $this->caches[$key] = $cache; - } + return $this->caches[$key] = $cache; + } - /** - * Returns the cache options by key - * - * @param string $key - * @return array - */ - protected function cacheOptions(string $key): array - { - $options = $this->option($this->cacheOptionsKey($key), false); + /** + * Returns the cache options by key + * + * @param string $key + * @return array + */ + protected function cacheOptions(string $key): array + { + $options = $this->option($this->cacheOptionsKey($key), false); - if ($options === false) { - return [ - 'active' => false - ]; - } + if ($options === false) { + return [ + 'active' => false + ]; + } - $prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) . - '/' . - str_replace('.', '/', $key); + $prefix = str_replace(['/', ':'], '_', $this->system()->indexUrl()) . + '/' . + str_replace('.', '/', $key); - $defaults = [ - 'active' => true, - 'type' => 'file', - 'extension' => 'cache', - 'root' => $this->root('cache'), - 'prefix' => $prefix - ]; + $defaults = [ + 'active' => true, + 'type' => 'file', + 'extension' => 'cache', + 'root' => $this->root('cache'), + 'prefix' => $prefix + ]; - if ($options === true) { - return $defaults; - } else { - return array_merge($defaults, $options); - } - } + if ($options === true) { + return $defaults; + } else { + return array_merge($defaults, $options); + } + } - /** - * Takes care of converting prefixed plugin cache setups - * to the right cache key, while leaving regular cache - * setups untouched. - * - * @param string $key - * @return string - */ - protected function cacheOptionsKey(string $key): string - { - $prefixedKey = 'cache.' . $key; + /** + * Takes care of converting prefixed plugin cache setups + * to the right cache key, while leaving regular cache + * setups untouched. + * + * @param string $key + * @return string + */ + protected function cacheOptionsKey(string $key): string + { + $prefixedKey = 'cache.' . $key; - if (isset($this->options[$prefixedKey])) { - return $prefixedKey; - } + if (isset($this->options[$prefixedKey])) { + return $prefixedKey; + } - // plain keys without dots don't need further investigation - // since they can never be from a plugin. - if (strpos($key, '.') === false) { - return $prefixedKey; - } + // plain keys without dots don't need further investigation + // since they can never be from a plugin. + if (strpos($key, '.') === false) { + return $prefixedKey; + } - // try to extract the plugin name - $parts = explode('.', $key); - $pluginName = implode('/', array_slice($parts, 0, 2)); - $pluginPrefix = implode('.', array_slice($parts, 0, 2)); - $cacheName = implode('.', array_slice($parts, 2)); + // try to extract the plugin name + $parts = explode('.', $key); + $pluginName = implode('/', array_slice($parts, 0, 2)); + $pluginPrefix = implode('.', array_slice($parts, 0, 2)); + $cacheName = implode('.', array_slice($parts, 2)); - // check if such a plugin exists - if ($this->plugin($pluginName)) { - return empty($cacheName) === true ? $pluginPrefix . '.cache' : $pluginPrefix . '.cache.' . $cacheName; - } + // check if such a plugin exists + if ($this->plugin($pluginName)) { + return empty($cacheName) === true ? $pluginPrefix . '.cache' : $pluginPrefix . '.cache.' . $cacheName; + } - return $prefixedKey; - } + return $prefixedKey; + } } diff --git a/kirby/src/Cms/AppErrors.php b/kirby/src/Cms/AppErrors.php index b6c1d5b..a1013a5 100644 --- a/kirby/src/Cms/AppErrors.php +++ b/kirby/src/Cms/AppErrors.php @@ -2,6 +2,7 @@ namespace Kirby\Cms; +use Kirby\Filesystem\F; use Kirby\Http\Response; use Kirby\Toolkit\I18n; use Whoops\Handler\CallbackHandler; @@ -21,184 +22,184 @@ use Whoops\Run as Whoops; */ trait AppErrors { - /** - * Whoops instance cache - * - * @var \Whoops\Run - */ - protected $whoops; + /** + * Whoops instance cache + * + * @var \Whoops\Run + */ + protected $whoops; - /** - * Registers the PHP error handler for CLI usage - * - * @return void - */ - protected function handleCliErrors(): void - { - $this->setWhoopsHandler(new PlainTextHandler()); - } + /** + * Registers the PHP error handler for CLI usage + * + * @return void + */ + protected function handleCliErrors(): void + { + $this->setWhoopsHandler(new PlainTextHandler()); + } - /** - * Registers the PHP error handler - * based on the environment - * - * @return void - */ - protected function handleErrors(): void - { - if ($this->request()->cli() === true) { - $this->handleCliErrors(); - return; - } + /** + * Registers the PHP error handler + * based on the environment + * + * @return void + */ + protected function handleErrors(): void + { + if ($this->environment()->cli() === true) { + $this->handleCliErrors(); + return; + } - if ($this->visitor()->prefersJson() === true) { - $this->handleJsonErrors(); - return; - } + if ($this->visitor()->prefersJson() === true) { + $this->handleJsonErrors(); + return; + } - $this->handleHtmlErrors(); - } + $this->handleHtmlErrors(); + } - /** - * Registers the PHP error handler for HTML output - * - * @return void - */ - protected function handleHtmlErrors(): void - { - $handler = null; + /** + * Registers the PHP error handler for HTML output + * + * @return void + */ + protected function handleHtmlErrors(): void + { + $handler = null; - if ($this->option('debug') === true) { - if ($this->option('whoops', true) === true) { - $handler = new PrettyPageHandler(); - $handler->setPageTitle('Kirby CMS Debugger'); - $handler->setResourcesPath(dirname(__DIR__, 2) . '/assets'); - $handler->addCustomCss('whoops.css'); + if ($this->option('debug') === true) { + if ($this->option('whoops', true) === true) { + $handler = new PrettyPageHandler(); + $handler->setPageTitle('Kirby CMS Debugger'); + $handler->setResourcesPath(dirname(__DIR__, 2) . '/assets'); + $handler->addCustomCss('whoops.css'); - if ($editor = $this->option('editor')) { - $handler->setEditor($editor); - } - } - } else { - $handler = new CallbackHandler(function ($exception, $inspector, $run) { - $fatal = $this->option('fatal'); + if ($editor = $this->option('editor')) { + $handler->setEditor($editor); + } + } + } else { + $handler = new CallbackHandler(function ($exception, $inspector, $run) { + $fatal = $this->option('fatal'); - if (is_a($fatal, 'Closure') === true) { - echo $fatal($this, $exception); - } else { - include $this->root('kirby') . '/views/fatal.php'; - } + if (is_a($fatal, 'Closure') === true) { + echo $fatal($this, $exception); + } else { + include $this->root('kirby') . '/views/fatal.php'; + } - return Handler::QUIT; - }); - } + return Handler::QUIT; + }); + } - if ($handler !== null) { - $this->setWhoopsHandler($handler); - } else { - $this->unsetWhoopsHandler(); - } - } + if ($handler !== null) { + $this->setWhoopsHandler($handler); + } else { + $this->unsetWhoopsHandler(); + } + } - /** - * Registers the PHP error handler for JSON output - * - * @return void - */ - protected function handleJsonErrors(): void - { - $handler = new CallbackHandler(function ($exception, $inspector, $run) { - if (is_a($exception, 'Kirby\Exception\Exception') === true) { - $httpCode = $exception->getHttpCode(); - $code = $exception->getCode(); - $details = $exception->getDetails(); - } elseif (is_a($exception, '\Throwable') === true) { - $httpCode = 500; - $code = $exception->getCode(); - $details = null; - } else { - $httpCode = 500; - $code = 500; - $details = null; - } + /** + * Registers the PHP error handler for JSON output + * + * @return void + */ + protected function handleJsonErrors(): void + { + $handler = new CallbackHandler(function ($exception, $inspector, $run) { + if (is_a($exception, 'Kirby\Exception\Exception') === true) { + $httpCode = $exception->getHttpCode(); + $code = $exception->getCode(); + $details = $exception->getDetails(); + } elseif (is_a($exception, '\Throwable') === true) { + $httpCode = 500; + $code = $exception->getCode(); + $details = null; + } else { + $httpCode = 500; + $code = 500; + $details = null; + } - if ($this->option('debug') === true) { - echo Response::json([ - 'status' => 'error', - 'exception' => get_class($exception), - 'code' => $code, - 'message' => $exception->getMessage(), - 'details' => $details, - 'file' => ltrim($exception->getFile(), $_SERVER['DOCUMENT_ROOT'] ?? ''), - 'line' => $exception->getLine(), - ], $httpCode); - } else { - echo Response::json([ - 'status' => 'error', - 'code' => $code, - 'details' => $details, - 'message' => I18n::translate('error.unexpected'), - ], $httpCode); - } + if ($this->option('debug') === true) { + echo Response::json([ + 'status' => 'error', + 'exception' => get_class($exception), + 'code' => $code, + 'message' => $exception->getMessage(), + 'details' => $details, + 'file' => F::relativepath($exception->getFile(), $this->environment()->get('DOCUMENT_ROOT', '')), + 'line' => $exception->getLine(), + ], $httpCode); + } else { + echo Response::json([ + 'status' => 'error', + 'code' => $code, + 'details' => $details, + 'message' => I18n::translate('error.unexpected'), + ], $httpCode); + } - return Handler::QUIT; - }); + return Handler::QUIT; + }); - $this->setWhoopsHandler($handler); - $this->whoops()->sendHttpCode(false); - } + $this->setWhoopsHandler($handler); + $this->whoops()->sendHttpCode(false); + } - /** - * Enables Whoops with the specified handler - * - * @param Callable|\Whoops\Handler\HandlerInterface $handler - * @return void - */ - protected function setWhoopsHandler($handler): void - { - $whoops = $this->whoops(); - $whoops->clearHandlers(); - $whoops->pushHandler($handler); - $whoops->pushHandler($this->getExceptionHookWhoopsHandler()); - $whoops->register(); // will only do something if not already registered - } + /** + * Enables Whoops with the specified handler + * + * @param Callable|\Whoops\Handler\HandlerInterface $handler + * @return void + */ + protected function setWhoopsHandler($handler): void + { + $whoops = $this->whoops(); + $whoops->clearHandlers(); + $whoops->pushHandler($handler); + $whoops->pushHandler($this->getExceptionHookWhoopsHandler()); + $whoops->register(); // will only do something if not already registered + } - /** - * Initializes a callback handler for triggering the `system.exception` hook - * - * @return \Whoops\Handler\CallbackHandler - */ - protected function getExceptionHookWhoopsHandler(): CallbackHandler - { - return new CallbackHandler(function ($exception, $inspector, $run) { - $this->trigger('system.exception', compact('exception')); - return Handler::DONE; - }); - } + /** + * Initializes a callback handler for triggering the `system.exception` hook + * + * @return \Whoops\Handler\CallbackHandler + */ + protected function getExceptionHookWhoopsHandler(): CallbackHandler + { + return new CallbackHandler(function ($exception, $inspector, $run) { + $this->trigger('system.exception', compact('exception')); + return Handler::DONE; + }); + } - /** - * Clears the Whoops handlers and disables Whoops - * - * @return void - */ - protected function unsetWhoopsHandler(): void - { - $whoops = $this->whoops(); - $whoops->clearHandlers(); - $whoops->unregister(); // will only do something if currently registered - } + /** + * Clears the Whoops handlers and disables Whoops + * + * @return void + */ + protected function unsetWhoopsHandler(): void + { + $whoops = $this->whoops(); + $whoops->clearHandlers(); + $whoops->unregister(); // will only do something if currently registered + } - /** - * Returns the Whoops error handler instance - * - * @return \Whoops\Run - */ - protected function whoops() - { - if ($this->whoops !== null) { - return $this->whoops; - } + /** + * Returns the Whoops error handler instance + * + * @return \Whoops\Run + */ + protected function whoops() + { + if ($this->whoops !== null) { + return $this->whoops; + } - return $this->whoops = new Whoops(); - } + return $this->whoops = new Whoops(); + } } diff --git a/kirby/src/Cms/AppPlugins.php b/kirby/src/Cms/AppPlugins.php index 44e90a1..161fb2b 100644 --- a/kirby/src/Cms/AppPlugins.php +++ b/kirby/src/Cms/AppPlugins.php @@ -26,865 +26,890 @@ use Kirby\Toolkit\V; */ trait AppPlugins { - /** - * A list of all registered plugins - * - * @var array - */ - protected static $plugins = []; - - /** - * The extension registry - * - * @var array - */ - protected $extensions = [ - // load options first to make them available for the rest - 'options' => [], - - // other plugin types - 'api' => [], - 'areas' => [], - 'authChallenges' => [], - 'blockMethods' => [], - 'blockModels' => [], - 'blocksMethods' => [], - 'blueprints' => [], - 'cacheTypes' => [], - 'collections' => [], - 'components' => [], - 'controllers' => [], - 'collectionFilters' => [], - 'collectionMethods' => [], - 'fieldMethods' => [], - 'fileMethods' => [], - 'fileTypes' => [], - 'filesMethods' => [], - 'fields' => [], - 'hooks' => [], - 'layoutMethods' => [], - 'layoutColumnMethods' => [], - 'layoutsMethods' => [], - 'pages' => [], - 'pageMethods' => [], - 'pagesMethods' => [], - 'pageModels' => [], - 'permissions' => [], - 'routes' => [], - 'sections' => [], - 'siteMethods' => [], - 'snippets' => [], - 'tags' => [], - 'templates' => [], - 'thirdParty' => [], - 'translations' => [], - 'userMethods' => [], - 'userModels' => [], - 'usersMethods' => [], - 'validators' => [], - ]; - - /** - * Flag when plugins have been loaded - * to not load them again - * - * @var bool - */ - protected $pluginsAreLoaded = false; - - /** - * Register all given extensions - * - * @internal - * @param array $extensions - * @param \Kirby\Cms\Plugin $plugin|null The plugin which defined those extensions - * @return array - */ - public function extend(array $extensions, Plugin $plugin = null): array - { - foreach ($this->extensions as $type => $registered) { - if (isset($extensions[$type]) === true) { - $this->{'extend' . $type}($extensions[$type], $plugin); - } - } - - return $this->extensions; - } - - /** - * Registers API extensions - * - * @param array|bool $api - * @return array - */ - protected function extendApi($api): array - { - if (is_array($api) === true) { - if (is_a($api['routes'] ?? [], 'Closure') === true) { - $api['routes'] = $api['routes']($this); - } - - return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND); - } else { - return $this->extensions['api']; - } - } - - /** - * Registers additional custom Panel areas - * - * @param array $areas - * @return array - */ - protected function extendAreas(array $areas): array - { - foreach ($areas as $id => $area) { - if (isset($this->extensions['areas'][$id]) === false) { - $this->extensions['areas'][$id] = []; - } - - $this->extensions['areas'][$id][] = $area; - } - - return $this->extensions['areas']; - } - - /** - * Registers additional authentication challenges - * - * @param array $challenges - * @return array - */ - protected function extendAuthChallenges(array $challenges): array - { - return $this->extensions['authChallenges'] = Auth::$challenges = array_merge(Auth::$challenges, $challenges); - } - - /** - * Registers additional block methods - * - * @param array $methods - * @return array - */ - protected function extendBlockMethods(array $methods): array - { - return $this->extensions['blockMethods'] = Block::$methods = array_merge(Block::$methods, $methods); - } - - /** - * Registers additional block models - * - * @param array $models - * @return array - */ - protected function extendBlockModels(array $models): array - { - return $this->extensions['blockModels'] = Block::$models = array_merge(Block::$models, $models); - } - - /** - * Registers additional blocks methods - * - * @param array $methods - * @return array - */ - protected function extendBlocksMethods(array $methods): array - { - return $this->extensions['blockMethods'] = Blocks::$methods = array_merge(Blocks::$methods, $methods); - } - - /** - * Registers additional blueprints - * - * @param array $blueprints - * @return array - */ - protected function extendBlueprints(array $blueprints): array - { - return $this->extensions['blueprints'] = array_merge($this->extensions['blueprints'], $blueprints); - } - - /** - * Registers additional cache types - * - * @param array $cacheTypes - * @return array - */ - protected function extendCacheTypes(array $cacheTypes): array - { - return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes); - } - - /** - * Registers additional collection filters - * - * @param array $filters - * @return array - */ - protected function extendCollectionFilters(array $filters): array - { - return $this->extensions['collectionFilters'] = ToolkitCollection::$filters = array_merge(ToolkitCollection::$filters, $filters); - } - - /** - * Registers additional collection methods - * - * @param array $methods - * @return array - */ - protected function extendCollectionMethods(array $methods): array - { - return $this->extensions['collectionMethods'] = Collection::$methods = array_merge(Collection::$methods, $methods); - } - - /** - * Registers additional collections - * - * @param array $collections - * @return array - */ - protected function extendCollections(array $collections): array - { - return $this->extensions['collections'] = array_merge($this->extensions['collections'], $collections); - } - - /** - * Registers core components - * - * @param array $components - * @return array - */ - protected function extendComponents(array $components): array - { - return $this->extensions['components'] = array_merge($this->extensions['components'], $components); - } - - /** - * Registers additional controllers - * - * @param array $controllers - * @return array - */ - protected function extendControllers(array $controllers): array - { - return $this->extensions['controllers'] = array_merge($this->extensions['controllers'], $controllers); - } - - /** - * Registers additional file methods - * - * @param array $methods - * @return array - */ - protected function extendFileMethods(array $methods): array - { - return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods); - } - - /** - * Registers additional custom file types and mimes - * - * @param array $fileTypes - * @return array - */ - protected function extendFileTypes(array $fileTypes): array - { - // normalize array - foreach ($fileTypes as $ext => $file) { - $extension = $file['extension'] ?? $ext; - $type = $file['type'] ?? null; - $mime = $file['mime'] ?? null; - $resizable = $file['resizable'] ?? false; - $viewable = $file['viewable'] ?? false; - - if (is_string($type) === true) { - if (isset(F::$types[$type]) === false) { - F::$types[$type] = []; - } - - if (in_array($extension, F::$types[$type]) === false) { - F::$types[$type][] = $extension; - } - } - - if ($mime !== null) { - if (array_key_exists($extension, Mime::$types) === true) { - // if `Mime::$types[$extension]` is not already an array, make it one - // and append the new MIME type unless it's already in the list - Mime::$types[$extension] = array_unique(array_merge((array)Mime::$types[$extension], (array)$mime)); - } else { - Mime::$types[$extension] = $mime; - } - } - - if ($resizable === true && in_array($extension, Image::$resizableTypes) === false) { - Image::$resizableTypes[] = $extension; - } - - if ($viewable === true && in_array($extension, Image::$viewableTypes) === false) { - Image::$viewableTypes[] = $extension; - } - } - - return $this->extensions['fileTypes'] = [ - 'type' => F::$types, - 'mime' => Mime::$types, - 'resizable' => Image::$resizableTypes, - 'viewable' => Image::$viewableTypes - ]; - } - - /** - * Registers additional files methods - * - * @param array $methods - * @return array - */ - protected function extendFilesMethods(array $methods): array - { - return $this->extensions['filesMethods'] = Files::$methods = array_merge(Files::$methods, $methods); - } - - /** - * Registers additional field methods - * - * @param array $methods - * @return array - */ - protected function extendFieldMethods(array $methods): array - { - return $this->extensions['fieldMethods'] = Field::$methods = array_merge(Field::$methods, array_change_key_case($methods)); - } - - /** - * Registers Panel fields - * - * @param array $fields - * @return array - */ - protected function extendFields(array $fields): array - { - return $this->extensions['fields'] = FormField::$types = array_merge(FormField::$types, $fields); - } - - /** - * Registers hooks - * - * @param array $hooks - * @return array - */ - protected function extendHooks(array $hooks): array - { - foreach ($hooks as $name => $callbacks) { - if (isset($this->extensions['hooks'][$name]) === false) { - $this->extensions['hooks'][$name] = []; - } - - if (is_array($callbacks) === false) { - $callbacks = [$callbacks]; - } - - foreach ($callbacks as $callback) { - $this->extensions['hooks'][$name][] = $callback; - } - } - - return $this->extensions['hooks']; - } - - /** - * Registers markdown component - * - * @param Closure $markdown - * @return Closure - */ - protected function extendMarkdown(Closure $markdown) - { - return $this->extensions['markdown'] = $markdown; - } - - /** - * Registers additional layout methods - * - * @param array $methods - * @return array - */ - protected function extendLayoutMethods(array $methods): array - { - return $this->extensions['layoutMethods'] = Layout::$methods = array_merge(Layout::$methods, $methods); - } - - /** - * Registers additional layout column methods - * - * @param array $methods - * @return array - */ - protected function extendLayoutColumnMethods(array $methods): array - { - return $this->extensions['layoutColumnMethods'] = LayoutColumn::$methods = array_merge(LayoutColumn::$methods, $methods); - } - - /** - * Registers additional layouts methods - * - * @param array $methods - * @return array - */ - protected function extendLayoutsMethods(array $methods): array - { - return $this->extensions['layoutsMethods'] = Layouts::$methods = array_merge(Layouts::$methods, $methods); - } - - /** - * Registers additional options - * - * @param array $options - * @param \Kirby\Cms\Plugin|null $plugin - * @return array - */ - protected function extendOptions(array $options, Plugin $plugin = null): array - { - if ($plugin !== null) { - $options = [$plugin->prefix() => $options]; - } - - return $this->extensions['options'] = $this->options = A::merge($options, $this->options, A::MERGE_REPLACE); - } - - /** - * Registers additional page methods - * - * @param array $methods - * @return array - */ - protected function extendPageMethods(array $methods): array - { - return $this->extensions['pageMethods'] = Page::$methods = array_merge(Page::$methods, $methods); - } - - /** - * Registers additional pages methods - * - * @param array $methods - * @return array - */ - protected function extendPagesMethods(array $methods): array - { - return $this->extensions['pagesMethods'] = Pages::$methods = array_merge(Pages::$methods, $methods); - } - - /** - * Registers additional page models - * - * @param array $models - * @return array - */ - protected function extendPageModels(array $models): array - { - return $this->extensions['pageModels'] = Page::$models = array_merge(Page::$models, $models); - } - - /** - * Registers pages - * - * @param array $pages - * @return array - */ - protected function extendPages(array $pages): array - { - return $this->extensions['pages'] = array_merge($this->extensions['pages'], $pages); - } - - /** - * Registers additional permissions - * - * @param array $permissions - * @param \Kirby\Cms\Plugin|null $plugin - * @return array - */ - protected function extendPermissions(array $permissions, Plugin $plugin = null): array - { - if ($plugin !== null) { - $permissions = [$plugin->prefix() => $permissions]; - } - - return $this->extensions['permissions'] = Permissions::$extendedActions = array_merge(Permissions::$extendedActions, $permissions); - } - - /** - * Registers additional routes - * - * @param array|\Closure $routes - * @return array - */ - protected function extendRoutes($routes): array - { - if (is_a($routes, 'Closure') === true) { - $routes = $routes($this); - } - - return $this->extensions['routes'] = array_merge($this->extensions['routes'], $routes); - } - - /** - * Registers Panel sections - * - * @param array $sections - * @return array - */ - protected function extendSections(array $sections): array - { - return $this->extensions['sections'] = Section::$types = array_merge(Section::$types, $sections); - } - - /** - * Registers additional site methods - * - * @param array $methods - * @return array - */ - protected function extendSiteMethods(array $methods): array - { - return $this->extensions['siteMethods'] = Site::$methods = array_merge(Site::$methods, $methods); - } - - /** - * Registers SmartyPants component - * - * @param \Closure $smartypants - * @return \Closure - */ - protected function extendSmartypants(Closure $smartypants) - { - return $this->extensions['smartypants'] = $smartypants; - } - - /** - * Registers additional snippets - * - * @param array $snippets - * @return array - */ - protected function extendSnippets(array $snippets): array - { - return $this->extensions['snippets'] = array_merge($this->extensions['snippets'], $snippets); - } - - /** - * Registers additional KirbyTags - * - * @param array $tags - * @return array - */ - protected function extendTags(array $tags): array - { - return $this->extensions['tags'] = KirbyTag::$types = array_merge(KirbyTag::$types, array_change_key_case($tags)); - } - - /** - * Registers additional templates - * - * @param array $templates - * @return array - */ - protected function extendTemplates(array $templates): array - { - return $this->extensions['templates'] = array_merge($this->extensions['templates'], $templates); - } - - /** - * Registers translations - * - * @param array $translations - * @return array - */ - protected function extendTranslations(array $translations): array - { - return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations); - } - - /** - * Add third party extensions to the registry - * so they can be used as plugins for plugins - * for example. - * - * @param array $extensions - * @return array - */ - protected function extendThirdParty(array $extensions): array - { - return $this->extensions['thirdParty'] = array_replace_recursive($this->extensions['thirdParty'], $extensions); - } - - /** - * Registers additional user methods - * - * @param array $methods - * @return array - */ - protected function extendUserMethods(array $methods): array - { - return $this->extensions['userMethods'] = User::$methods = array_merge(User::$methods, $methods); - } - - /** - * Registers additional user models - * - * @param array $models - * @return array - */ - protected function extendUserModels(array $models): array - { - return $this->extensions['userModels'] = User::$models = array_merge(User::$models, $models); - } - - /** - * Registers additional users methods - * - * @param array $methods - * @return array - */ - protected function extendUsersMethods(array $methods): array - { - return $this->extensions['usersMethods'] = Users::$methods = array_merge(Users::$methods, $methods); - } - - /** - * Registers additional custom validators - * - * @param array $validators - * @return array - */ - protected function extendValidators(array $validators): array - { - return $this->extensions['validators'] = V::$validators = array_merge(V::$validators, $validators); - } - - /** - * Returns a given extension by type and name - * - * @internal - * @param string $type i.e. `'hooks'` - * @param string $name i.e. `'page.delete:before'` - * @param mixed $fallback - * @return mixed - */ - public function extension(string $type, string $name, $fallback = null) - { - return $this->extensions($type)[$name] ?? $fallback; - } - - /** - * Returns the extensions registry - * - * @internal - * @param string|null $type - * @return array - */ - public function extensions(string $type = null) - { - if ($type === null) { - return $this->extensions; - } - - return $this->extensions[$type] ?? []; - } - - /** - * Load extensions from site folders. - * This is only used for models for now, but - * could be extended later - */ - protected function extensionsFromFolders() - { - $models = []; - - foreach (glob($this->root('models') . '/*.php') as $model) { - $name = F::name($model); - $class = str_replace(['.', '-', '_'], '', $name) . 'Page'; - - // load the model class - F::loadOnce($model); - - if (class_exists($class) === true) { - $models[$name] = $class; - } - } - - $this->extendPageModels($models); - } - - /** - * Register extensions that could be located in - * the options array. I.e. hooks and routes can be - * setup from the config. - * - * @return void - */ - protected function extensionsFromOptions() - { - // register routes and hooks from options - $this->extend([ - 'api' => $this->options['api'] ?? [], - 'routes' => $this->options['routes'] ?? [], - 'hooks' => $this->options['hooks'] ?? [] - ]); - } - - /** - * Apply all plugin extensions - * - * @return void - */ - protected function extensionsFromPlugins() - { - // register all their extensions - foreach ($this->plugins() as $plugin) { - $extends = $plugin->extends(); - - if (empty($extends) === false) { - $this->extend($extends, $plugin); - } - } - } - - /** - * Apply all passed extensions - * - * @param array $props - * @return void - */ - protected function extensionsFromProps(array $props) - { - $this->extend($props); - } - - /** - * Apply all default extensions - * - * @return void - */ - protected function extensionsFromSystem() - { - // mixins - FormField::$mixins = $this->core->fieldMixins(); - Section::$mixins = $this->core->sectionMixins(); - - // aliases - KirbyTag::$aliases = $this->core->kirbyTagAliases(); - Field::$aliases = $this->core->fieldMethodAliases(); - - // blueprint presets - PageBlueprint::$presets = $this->core->blueprintPresets(); - - $this->extendAuthChallenges($this->core->authChallenges()); - $this->extendCacheTypes($this->core->cacheTypes()); - $this->extendComponents($this->core->components()); - $this->extendBlueprints($this->core->blueprints()); - $this->extendFields($this->core->fields()); - $this->extendFieldMethods($this->core->fieldMethods()); - $this->extendSections($this->core->sections()); - $this->extendSnippets($this->core->snippets()); - $this->extendTags($this->core->kirbyTags()); - $this->extendTemplates($this->core->templates()); - } - - /** - * Returns the native implementation - * of a core component - * - * @param string $component - * @return \Closure|false - */ - public function nativeComponent(string $component) - { - return $this->core->components()[$component] ?? false; - } - - /** - * Kirby plugin factory and getter - * - * @param string $name - * @param array|null $extends If null is passed it will be used as getter. Otherwise as factory. - * @return \Kirby\Cms\Plugin|null - * @throws \Kirby\Exception\DuplicateException - */ - public static function plugin(string $name, array $extends = null) - { - if ($extends === null) { - return static::$plugins[$name] ?? null; - } - - // get the correct root for the plugin - $extends['root'] = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); - - $plugin = new Plugin($name, $extends); - $name = $plugin->name(); - - if (isset(static::$plugins[$name]) === true) { - throw new DuplicateException('The plugin "' . $name . '" has already been registered'); - } - - return static::$plugins[$name] = $plugin; - } - - /** - * Loads and returns all plugins in the site/plugins directory - * Loading only happens on the first call. - * - * @internal - * @param array|null $plugins Can be used to overwrite the plugins registry - * @return array - */ - public function plugins(array $plugins = null): array - { - // overwrite the existing plugins registry - if ($plugins !== null) { - $this->pluginsAreLoaded = true; - return static::$plugins = $plugins; - } - - // don't load plugins twice - if ($this->pluginsAreLoaded === true) { - return static::$plugins; - } - - // load all plugins from site/plugins - $this->pluginsLoader(); - - // mark plugins as loaded to stop doing it twice - $this->pluginsAreLoaded = true; - return static::$plugins; - } - - /** - * Loads all plugins from site/plugins - * - * @return array Array of loaded directories - */ - protected function pluginsLoader(): array - { - $root = $this->root('plugins'); - $loaded = []; - - foreach (Dir::read($root) as $dirname) { - if (in_array(substr($dirname, 0, 1), ['.', '_']) === true) { - continue; - } - - $dir = $root . '/' . $dirname; - $entry = $dir . '/index.php'; - - if (is_dir($dir) !== true || is_file($entry) !== true) { - continue; - } - - F::loadOnce($entry); - - $loaded[] = $dir; - } - - return $loaded; - } + /** + * A list of all registered plugins + * + * @var array + */ + protected static $plugins = []; + + /** + * The extension registry + * + * @var array + */ + protected $extensions = [ + // load options first to make them available for the rest + 'options' => [], + + // other plugin types + 'api' => [], + 'areas' => [], + 'authChallenges' => [], + 'blockMethods' => [], + 'blockModels' => [], + 'blocksMethods' => [], + 'blueprints' => [], + 'cacheTypes' => [], + 'collections' => [], + 'components' => [], + 'controllers' => [], + 'collectionFilters' => [], + 'collectionMethods' => [], + 'fieldMethods' => [], + 'fileMethods' => [], + 'fileTypes' => [], + 'filesMethods' => [], + 'fields' => [], + 'hooks' => [], + 'layoutMethods' => [], + 'layoutColumnMethods' => [], + 'layoutsMethods' => [], + 'pages' => [], + 'pageMethods' => [], + 'pagesMethods' => [], + 'pageModels' => [], + 'permissions' => [], + 'routes' => [], + 'sections' => [], + 'siteMethods' => [], + 'snippets' => [], + 'tags' => [], + 'templates' => [], + 'thirdParty' => [], + 'translations' => [], + 'userMethods' => [], + 'userModels' => [], + 'usersMethods' => [], + 'validators' => [], + ]; + + /** + * Flag when plugins have been loaded + * to not load them again + * + * @var bool + */ + protected $pluginsAreLoaded = false; + + /** + * Register all given extensions + * + * @internal + * @param array $extensions + * @param \Kirby\Cms\Plugin $plugin|null The plugin which defined those extensions + * @return array + */ + public function extend(array $extensions, Plugin $plugin = null): array + { + foreach ($this->extensions as $type => $registered) { + if (isset($extensions[$type]) === true) { + $this->{'extend' . $type}($extensions[$type], $plugin); + } + } + + return $this->extensions; + } + + /** + * Registers API extensions + * + * @param array|bool $api + * @return array + */ + protected function extendApi($api): array + { + if (is_array($api) === true) { + if (is_a($api['routes'] ?? [], 'Closure') === true) { + $api['routes'] = $api['routes']($this); + } + + return $this->extensions['api'] = A::merge($this->extensions['api'], $api, A::MERGE_APPEND); + } else { + return $this->extensions['api']; + } + } + + /** + * Registers additional custom Panel areas + * + * @param array $areas + * @return array + */ + protected function extendAreas(array $areas): array + { + foreach ($areas as $id => $area) { + if (isset($this->extensions['areas'][$id]) === false) { + $this->extensions['areas'][$id] = []; + } + + $this->extensions['areas'][$id][] = $area; + } + + return $this->extensions['areas']; + } + + /** + * Registers additional authentication challenges + * + * @param array $challenges + * @return array + */ + protected function extendAuthChallenges(array $challenges): array + { + return $this->extensions['authChallenges'] = Auth::$challenges = array_merge(Auth::$challenges, $challenges); + } + + /** + * Registers additional block methods + * + * @param array $methods + * @return array + */ + protected function extendBlockMethods(array $methods): array + { + return $this->extensions['blockMethods'] = Block::$methods = array_merge(Block::$methods, $methods); + } + + /** + * Registers additional block models + * + * @param array $models + * @return array + */ + protected function extendBlockModels(array $models): array + { + return $this->extensions['blockModels'] = Block::$models = array_merge(Block::$models, $models); + } + + /** + * Registers additional blocks methods + * + * @param array $methods + * @return array + */ + protected function extendBlocksMethods(array $methods): array + { + return $this->extensions['blockMethods'] = Blocks::$methods = array_merge(Blocks::$methods, $methods); + } + + /** + * Registers additional blueprints + * + * @param array $blueprints + * @return array + */ + protected function extendBlueprints(array $blueprints): array + { + return $this->extensions['blueprints'] = array_merge($this->extensions['blueprints'], $blueprints); + } + + /** + * Registers additional cache types + * + * @param array $cacheTypes + * @return array + */ + protected function extendCacheTypes(array $cacheTypes): array + { + return $this->extensions['cacheTypes'] = array_merge($this->extensions['cacheTypes'], $cacheTypes); + } + + /** + * Registers additional collection filters + * + * @param array $filters + * @return array + */ + protected function extendCollectionFilters(array $filters): array + { + return $this->extensions['collectionFilters'] = ToolkitCollection::$filters = array_merge(ToolkitCollection::$filters, $filters); + } + + /** + * Registers additional collection methods + * + * @param array $methods + * @return array + */ + protected function extendCollectionMethods(array $methods): array + { + return $this->extensions['collectionMethods'] = Collection::$methods = array_merge(Collection::$methods, $methods); + } + + /** + * Registers additional collections + * + * @param array $collections + * @return array + */ + protected function extendCollections(array $collections): array + { + return $this->extensions['collections'] = array_merge($this->extensions['collections'], $collections); + } + + /** + * Registers core components + * + * @param array $components + * @return array + */ + protected function extendComponents(array $components): array + { + return $this->extensions['components'] = array_merge($this->extensions['components'], $components); + } + + /** + * Registers additional controllers + * + * @param array $controllers + * @return array + */ + protected function extendControllers(array $controllers): array + { + return $this->extensions['controllers'] = array_merge($this->extensions['controllers'], $controllers); + } + + /** + * Registers additional file methods + * + * @param array $methods + * @return array + */ + protected function extendFileMethods(array $methods): array + { + return $this->extensions['fileMethods'] = File::$methods = array_merge(File::$methods, $methods); + } + + /** + * Registers additional custom file types and mimes + * + * @param array $fileTypes + * @return array + */ + protected function extendFileTypes(array $fileTypes): array + { + // normalize array + foreach ($fileTypes as $ext => $file) { + $extension = $file['extension'] ?? $ext; + $type = $file['type'] ?? null; + $mime = $file['mime'] ?? null; + $resizable = $file['resizable'] ?? false; + $viewable = $file['viewable'] ?? false; + + if (is_string($type) === true) { + if (isset(F::$types[$type]) === false) { + F::$types[$type] = []; + } + + if (in_array($extension, F::$types[$type]) === false) { + F::$types[$type][] = $extension; + } + } + + if ($mime !== null) { + if (array_key_exists($extension, Mime::$types) === true) { + // if `Mime::$types[$extension]` is not already an array, make it one + // and append the new MIME type unless it's already in the list + Mime::$types[$extension] = array_unique(array_merge((array)Mime::$types[$extension], (array)$mime)); + } else { + Mime::$types[$extension] = $mime; + } + } + + if ($resizable === true && in_array($extension, Image::$resizableTypes) === false) { + Image::$resizableTypes[] = $extension; + } + + if ($viewable === true && in_array($extension, Image::$viewableTypes) === false) { + Image::$viewableTypes[] = $extension; + } + } + + return $this->extensions['fileTypes'] = [ + 'type' => F::$types, + 'mime' => Mime::$types, + 'resizable' => Image::$resizableTypes, + 'viewable' => Image::$viewableTypes + ]; + } + + /** + * Registers additional files methods + * + * @param array $methods + * @return array + */ + protected function extendFilesMethods(array $methods): array + { + return $this->extensions['filesMethods'] = Files::$methods = array_merge(Files::$methods, $methods); + } + + /** + * Registers additional field methods + * + * @param array $methods + * @return array + */ + protected function extendFieldMethods(array $methods): array + { + return $this->extensions['fieldMethods'] = Field::$methods = array_merge(Field::$methods, array_change_key_case($methods)); + } + + /** + * Registers Panel fields + * + * @param array $fields + * @return array + */ + protected function extendFields(array $fields): array + { + return $this->extensions['fields'] = FormField::$types = array_merge(FormField::$types, $fields); + } + + /** + * Registers hooks + * + * @param array $hooks + * @return array + */ + protected function extendHooks(array $hooks): array + { + foreach ($hooks as $name => $callbacks) { + if (isset($this->extensions['hooks'][$name]) === false) { + $this->extensions['hooks'][$name] = []; + } + + if (is_array($callbacks) === false) { + $callbacks = [$callbacks]; + } + + foreach ($callbacks as $callback) { + $this->extensions['hooks'][$name][] = $callback; + } + } + + return $this->extensions['hooks']; + } + + /** + * Registers markdown component + * + * @param Closure $markdown + * @return Closure + */ + protected function extendMarkdown(Closure $markdown) + { + return $this->extensions['markdown'] = $markdown; + } + + /** + * Registers additional layout methods + * + * @param array $methods + * @return array + */ + protected function extendLayoutMethods(array $methods): array + { + return $this->extensions['layoutMethods'] = Layout::$methods = array_merge(Layout::$methods, $methods); + } + + /** + * Registers additional layout column methods + * + * @param array $methods + * @return array + */ + protected function extendLayoutColumnMethods(array $methods): array + { + return $this->extensions['layoutColumnMethods'] = LayoutColumn::$methods = array_merge(LayoutColumn::$methods, $methods); + } + + /** + * Registers additional layouts methods + * + * @param array $methods + * @return array + */ + protected function extendLayoutsMethods(array $methods): array + { + return $this->extensions['layoutsMethods'] = Layouts::$methods = array_merge(Layouts::$methods, $methods); + } + + /** + * Registers additional options + * + * @param array $options + * @param \Kirby\Cms\Plugin|null $plugin + * @return array + */ + protected function extendOptions(array $options, Plugin $plugin = null): array + { + if ($plugin !== null) { + $options = [$plugin->prefix() => $options]; + } + + return $this->extensions['options'] = $this->options = A::merge($options, $this->options, A::MERGE_REPLACE); + } + + /** + * Registers additional page methods + * + * @param array $methods + * @return array + */ + protected function extendPageMethods(array $methods): array + { + return $this->extensions['pageMethods'] = Page::$methods = array_merge(Page::$methods, $methods); + } + + /** + * Registers additional pages methods + * + * @param array $methods + * @return array + */ + protected function extendPagesMethods(array $methods): array + { + return $this->extensions['pagesMethods'] = Pages::$methods = array_merge(Pages::$methods, $methods); + } + + /** + * Registers additional page models + * + * @param array $models + * @return array + */ + protected function extendPageModels(array $models): array + { + return $this->extensions['pageModels'] = Page::$models = array_merge(Page::$models, $models); + } + + /** + * Registers pages + * + * @param array $pages + * @return array + */ + protected function extendPages(array $pages): array + { + return $this->extensions['pages'] = array_merge($this->extensions['pages'], $pages); + } + + /** + * Registers additional permissions + * + * @param array $permissions + * @param \Kirby\Cms\Plugin|null $plugin + * @return array + */ + protected function extendPermissions(array $permissions, Plugin $plugin = null): array + { + if ($plugin !== null) { + $permissions = [$plugin->prefix() => $permissions]; + } + + return $this->extensions['permissions'] = Permissions::$extendedActions = array_merge(Permissions::$extendedActions, $permissions); + } + + /** + * Registers additional routes + * + * @param array|\Closure $routes + * @return array + */ + protected function extendRoutes($routes): array + { + if (is_a($routes, 'Closure') === true) { + $routes = $routes($this); + } + + return $this->extensions['routes'] = array_merge($this->extensions['routes'], $routes); + } + + /** + * Registers Panel sections + * + * @param array $sections + * @return array + */ + protected function extendSections(array $sections): array + { + return $this->extensions['sections'] = Section::$types = array_merge(Section::$types, $sections); + } + + /** + * Registers additional site methods + * + * @param array $methods + * @return array + */ + protected function extendSiteMethods(array $methods): array + { + return $this->extensions['siteMethods'] = Site::$methods = array_merge(Site::$methods, $methods); + } + + /** + * Registers SmartyPants component + * + * @param \Closure $smartypants + * @return \Closure + */ + protected function extendSmartypants(Closure $smartypants) + { + return $this->extensions['smartypants'] = $smartypants; + } + + /** + * Registers additional snippets + * + * @param array $snippets + * @return array + */ + protected function extendSnippets(array $snippets): array + { + return $this->extensions['snippets'] = array_merge($this->extensions['snippets'], $snippets); + } + + /** + * Registers additional KirbyTags + * + * @param array $tags + * @return array + */ + protected function extendTags(array $tags): array + { + return $this->extensions['tags'] = KirbyTag::$types = array_merge(KirbyTag::$types, array_change_key_case($tags)); + } + + /** + * Registers additional templates + * + * @param array $templates + * @return array + */ + protected function extendTemplates(array $templates): array + { + return $this->extensions['templates'] = array_merge($this->extensions['templates'], $templates); + } + + /** + * Registers translations + * + * @param array $translations + * @return array + */ + protected function extendTranslations(array $translations): array + { + return $this->extensions['translations'] = array_replace_recursive($this->extensions['translations'], $translations); + } + + /** + * Add third party extensions to the registry + * so they can be used as plugins for plugins + * for example. + * + * @param array $extensions + * @return array + */ + protected function extendThirdParty(array $extensions): array + { + return $this->extensions['thirdParty'] = array_replace_recursive($this->extensions['thirdParty'], $extensions); + } + + /** + * Registers additional user methods + * + * @param array $methods + * @return array + */ + protected function extendUserMethods(array $methods): array + { + return $this->extensions['userMethods'] = User::$methods = array_merge(User::$methods, $methods); + } + + /** + * Registers additional user models + * + * @param array $models + * @return array + */ + protected function extendUserModels(array $models): array + { + return $this->extensions['userModels'] = User::$models = array_merge(User::$models, $models); + } + + /** + * Registers additional users methods + * + * @param array $methods + * @return array + */ + protected function extendUsersMethods(array $methods): array + { + return $this->extensions['usersMethods'] = Users::$methods = array_merge(Users::$methods, $methods); + } + + /** + * Registers additional custom validators + * + * @param array $validators + * @return array + */ + protected function extendValidators(array $validators): array + { + return $this->extensions['validators'] = V::$validators = array_merge(V::$validators, $validators); + } + + /** + * Returns a given extension by type and name + * + * @internal + * @param string $type i.e. `'hooks'` + * @param string $name i.e. `'page.delete:before'` + * @param mixed $fallback + * @return mixed + */ + public function extension(string $type, string $name, $fallback = null) + { + return $this->extensions($type)[$name] ?? $fallback; + } + + /** + * Returns the extensions registry + * + * @internal + * @param string|null $type + * @return array + */ + public function extensions(string $type = null) + { + if ($type === null) { + return $this->extensions; + } + + return $this->extensions[$type] ?? []; + } + + /** + * Load extensions from site folders. + * This is only used for models for now, but + * could be extended later + */ + protected function extensionsFromFolders() + { + $models = []; + + foreach (glob($this->root('models') . '/*.php') as $model) { + $name = F::name($model); + $class = str_replace(['.', '-', '_'], '', $name) . 'Page'; + + // load the model class + F::loadOnce($model); + + if (class_exists($class) === true) { + $models[$name] = $class; + } + } + + $this->extendPageModels($models); + } + + /** + * Register extensions that could be located in + * the options array. I.e. hooks and routes can be + * setup from the config. + * + * @return void + */ + protected function extensionsFromOptions() + { + // register routes and hooks from options + $this->extend([ + 'api' => $this->options['api'] ?? [], + 'routes' => $this->options['routes'] ?? [], + 'hooks' => $this->options['hooks'] ?? [] + ]); + } + + /** + * Apply all plugin extensions + * + * @return void + */ + protected function extensionsFromPlugins() + { + // register all their extensions + foreach ($this->plugins() as $plugin) { + $extends = $plugin->extends(); + + if (empty($extends) === false) { + $this->extend($extends, $plugin); + } + } + } + + /** + * Apply all passed extensions + * + * @param array $props + * @return void + */ + protected function extensionsFromProps(array $props) + { + $this->extend($props); + } + + /** + * Apply all default extensions + * + * @return void + */ + protected function extensionsFromSystem() + { + // mixins + FormField::$mixins = $this->core->fieldMixins(); + Section::$mixins = $this->core->sectionMixins(); + + // aliases + KirbyTag::$aliases = $this->core->kirbyTagAliases(); + Field::$aliases = $this->core->fieldMethodAliases(); + + // blueprint presets + PageBlueprint::$presets = $this->core->blueprintPresets(); + + $this->extendAuthChallenges($this->core->authChallenges()); + $this->extendCacheTypes($this->core->cacheTypes()); + $this->extendComponents($this->core->components()); + $this->extendBlueprints($this->core->blueprints()); + $this->extendFields($this->core->fields()); + $this->extendFieldMethods($this->core->fieldMethods()); + $this->extendSections($this->core->sections()); + $this->extendSnippets($this->core->snippets()); + $this->extendTags($this->core->kirbyTags()); + $this->extendTemplates($this->core->templates()); + } + + /** + * Checks if a native component was extended + * @since 3.7.0 + * + * @param string $component + * @return bool + */ + public function isNativeComponent(string $component): bool + { + return $this->component($component) === $this->nativeComponent($component); + } + + /** + * Returns the native implementation + * of a core component + * + * @param string $component + * @return \Closure|false + */ + public function nativeComponent(string $component) + { + return $this->core->components()[$component] ?? false; + } + + /** + * Kirby plugin factory and getter + * + * @param string $name + * @param array|null $extends If null is passed it will be used as getter. Otherwise as factory. + * @return \Kirby\Cms\Plugin|null + * @throws \Kirby\Exception\DuplicateException + */ + public static function plugin(string $name, array $extends = null) + { + if ($extends === null) { + return static::$plugins[$name] ?? null; + } + + // get the correct root for the plugin + $extends['root'] = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); + + $plugin = new Plugin($name, $extends); + $name = $plugin->name(); + + if (isset(static::$plugins[$name]) === true) { + throw new DuplicateException('The plugin "' . $name . '" has already been registered'); + } + + return static::$plugins[$name] = $plugin; + } + + /** + * Loads and returns all plugins in the site/plugins directory + * Loading only happens on the first call. + * + * @internal + * @param array|null $plugins Can be used to overwrite the plugins registry + * @return array + */ + public function plugins(array $plugins = null): array + { + // overwrite the existing plugins registry + if ($plugins !== null) { + $this->pluginsAreLoaded = true; + return static::$plugins = $plugins; + } + + // don't load plugins twice + if ($this->pluginsAreLoaded === true) { + return static::$plugins; + } + + // load all plugins from site/plugins + $this->pluginsLoader(); + + // mark plugins as loaded to stop doing it twice + $this->pluginsAreLoaded = true; + return static::$plugins; + } + + /** + * Loads all plugins from site/plugins + * + * @return array Array of loaded directories + */ + protected function pluginsLoader(): array + { + $root = $this->root('plugins'); + $loaded = []; + + foreach (Dir::read($root) as $dirname) { + if (in_array(substr($dirname, 0, 1), ['.', '_']) === true) { + continue; + } + + $dir = $root . '/' . $dirname; + + if (is_dir($dir) !== true) { + continue; + } + + $entry = $dir . '/index.php'; + $script = $dir . '/index.js'; + $styles = $dir . '/index.css'; + + if (is_file($entry) === true) { + F::loadOnce($entry); + } elseif (is_file($script) === true || is_file($styles) === true) { + // if no PHP file is present but an index.js or index.css, + // register as anonymous plugin (without actual extensions) + // to be picked up by the Panel\Document class when + // rendering the Panel view + static::plugin('plugins/' . $dirname, ['root' => $dir]); + } else { + continue; + } + + $loaded[] = $dir; + } + + return $loaded; + } } diff --git a/kirby/src/Cms/AppTranslations.php b/kirby/src/Cms/AppTranslations.php index 61f18ff..86d09c2 100644 --- a/kirby/src/Cms/AppTranslations.php +++ b/kirby/src/Cms/AppTranslations.php @@ -17,221 +17,205 @@ use Kirby\Toolkit\Str; */ trait AppTranslations { - protected $translations; + protected $translations; - /** - * Setup internationalization - * - * @return void - */ - protected function i18n(): void - { - I18n::$load = function ($locale): array { - $data = []; + /** + * Setup internationalization + * + * @return void + */ + protected function i18n(): void + { + I18n::$load = function ($locale): array { + $data = []; - if ($translation = $this->translation($locale)) { - $data = $translation->data(); - } + if ($translation = $this->translation($locale)) { + $data = $translation->data(); + } - // inject translations from the current language - if ( - $this->multilang() === true && - $language = $this->languages()->find($locale) - ) { - $data = array_merge($data, $language->translations()); - } + // inject translations from the current language + if ( + $this->multilang() === true && + $language = $this->languages()->find($locale) + ) { + $data = array_merge($data, $language->translations()); + } - return $data; - }; + return $data; + }; - // the actual locale is set using $app->setCurrentTranslation() - I18n::$locale = function (): string { - if ($this->multilang() === true) { - return $this->defaultLanguage()->code(); - } else { - return 'en'; - } - }; + // the actual locale is set using $app->setCurrentTranslation() + I18n::$locale = function (): string { + if ($this->multilang() === true) { + return $this->defaultLanguage()->code(); + } else { + return 'en'; + } + }; - I18n::$fallback = function (): array { - if ($this->multilang() === true) { - // first try to fall back to the configured default language - $defaultCode = $this->defaultLanguage()->code(); - $fallback = [$defaultCode]; + I18n::$fallback = function (): array { + if ($this->multilang() === true) { + // first try to fall back to the configured default language + $defaultCode = $this->defaultLanguage()->code(); + $fallback = [$defaultCode]; - // if the default language is specified with a country code - // (e.g. `en-us`), also try with just the language code - if (preg_match('/^([a-z]{2})-[a-z]+$/i', $defaultCode, $matches) === 1) { - $fallback[] = $matches[1]; - } + // if the default language is specified with a country code + // (e.g. `en-us`), also try with just the language code + if (preg_match('/^([a-z]{2})-[a-z]+$/i', $defaultCode, $matches) === 1) { + $fallback[] = $matches[1]; + } - // fall back to the complete English translation - // as a last resort - $fallback[] = 'en'; + // fall back to the complete English translation + // as a last resort + $fallback[] = 'en'; - return $fallback; - } else { - return ['en']; - } - }; + return $fallback; + } else { + return ['en']; + } + }; - I18n::$translations = []; + I18n::$translations = []; - // add slug rules based on config option - if ($slugs = $this->option('slugs')) { - // two ways that the option can be defined: - // "slugs" => "de" or "slugs" => ["language" => "de"] - if ($slugs = $slugs['language'] ?? $slugs ?? null) { - Str::$language = Language::loadRules($slugs); - } - } - } + // add slug rules based on config option + if ($slugs = $this->option('slugs')) { + // two ways that the option can be defined: + // "slugs" => "de" or "slugs" => ["language" => "de"] + if ($slugs = $slugs['language'] ?? $slugs ?? null) { + Str::$language = Language::loadRules($slugs); + } + } + } - /** - * Returns the language code that will be used - * for the Panel if no user is logged in or if - * no language is configured for the user - * - * @return string - */ - public function panelLanguage(): string - { - if ($this->multilang() === true) { - $defaultCode = $this->defaultLanguage()->code(); + /** + * Returns the language code that will be used + * for the Panel if no user is logged in or if + * no language is configured for the user + * + * @return string + */ + public function panelLanguage(): string + { + if ($this->multilang() === true) { + $defaultCode = $this->defaultLanguage()->code(); - // extract the language code from a language that - // contains the country code (e.g. `en-us`) - if (preg_match('/^([a-z]{2})-[a-z]+$/i', $defaultCode, $matches) === 1) { - $defaultCode = $matches[1]; - } - } else { - $defaultCode = 'en'; - } + // extract the language code from a language that + // contains the country code (e.g. `en-us`) + if (preg_match('/^([a-z]{2})-[a-z]+$/i', $defaultCode, $matches) === 1) { + $defaultCode = $matches[1]; + } + } else { + $defaultCode = 'en'; + } - return $this->option('panel.language', $defaultCode); - } + return $this->option('panel.language', $defaultCode); + } - /** - * Load and set the current language if it exists - * Otherwise fall back to the default language - * - * @internal - * @param string|null $languageCode - * @return \Kirby\Cms\Language|null - */ - public function setCurrentLanguage(string $languageCode = null) - { - if ($this->multilang() === false) { - Locale::set($this->option('locale', 'en_US.utf-8')); - return $this->language = null; - } + /** + * Load and set the current language if it exists + * Otherwise fall back to the default language + * + * @internal + * @param string|null $languageCode + * @return \Kirby\Cms\Language|null + */ + public function setCurrentLanguage(string $languageCode = null) + { + if ($this->multilang() === false) { + Locale::set($this->option('locale', 'en_US.utf-8')); + return $this->language = null; + } - if ($language = $this->language($languageCode)) { - $this->language = $language; - } else { - $this->language = $this->defaultLanguage(); - } + if ($language = $this->language($languageCode)) { + $this->language = $language; + } else { + $this->language = $this->defaultLanguage(); + } - if ($this->language) { - Locale::set($this->language->locale()); - } + if ($this->language) { + Locale::set($this->language->locale()); + } - // add language slug rules to Str class - Str::$language = $this->language->rules(); + // add language slug rules to Str class + Str::$language = $this->language->rules(); - return $this->language; - } + return $this->language; + } - /** - * Set the current translation - * - * @internal - * @param string|null $translationCode - * @return void - */ - public function setCurrentTranslation(string $translationCode = null): void - { - I18n::$locale = $translationCode ?? 'en'; - } + /** + * Set the current translation + * + * @internal + * @param string|null $translationCode + * @return void + */ + public function setCurrentTranslation(string $translationCode = null): void + { + I18n::$locale = $translationCode ?? 'en'; + } - /** - * Set locale settings - * - * @deprecated 3.5.0 Use `\Kirby\Toolkit\Locale::set()` instead - * @todo Remove in 3.7.0 - * - * @param string|array $locale - */ - public function setLocale($locale): void - { - // @codeCoverageIgnoreStart - deprecated('`Kirby\Cms\App::setLocale()` has been deprecated and will be removed in 3.7.0. Use `Kirby\Toolkit\Locale::set()` instead'); - Locale::set($locale); - // @codeCoverageIgnoreEnd - } + /** + * Load a specific translation by locale + * + * @param string|null $locale Locale name or `null` for the current locale + * @return \Kirby\Cms\Translation + */ + public function translation(?string $locale = null) + { + $locale = $locale ?? I18n::locale(); + $locale = basename($locale); - /** - * Load a specific translation by locale - * - * @param string|null $locale Locale name or `null` for the current locale - * @return \Kirby\Cms\Translation - */ - public function translation(?string $locale = null) - { - $locale = $locale ?? I18n::locale(); - $locale = basename($locale); + // prefer loading them from the translations collection + if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { + if ($translation = $this->translations()->find($locale)) { + return $translation; + } + } - // prefer loading them from the translations collection - if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { - if ($translation = $this->translations()->find($locale)) { - return $translation; - } - } + // get injected translation data from plugins etc. + $inject = $this->extensions['translations'][$locale] ?? []; - // get injected translation data from plugins etc. - $inject = $this->extensions['translations'][$locale] ?? []; + // inject current language translations + if ($language = $this->language($locale)) { + $inject = array_merge($inject, $language->translations()); + } - // inject current language translations - if ($language = $this->language($locale)) { - $inject = array_merge($inject, $language->translations()); - } + // load from disk instead + return Translation::load($locale, $this->root('i18n:translations') . '/' . $locale . '.json', $inject); + } - // load from disk instead - return Translation::load($locale, $this->root('i18n:translations') . '/' . $locale . '.json', $inject); - } + /** + * Returns all available translations + * + * @return \Kirby\Cms\Translations + */ + public function translations() + { + if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { + return $this->translations; + } - /** - * Returns all available translations - * - * @return \Kirby\Cms\Translations - */ - public function translations() - { - if (is_a($this->translations, 'Kirby\Cms\Translations') === true) { - return $this->translations; - } + $translations = $this->extensions['translations'] ?? []; - $translations = $this->extensions['translations'] ?? []; + // injects languages translations + if ($languages = $this->languages()) { + foreach ($languages as $language) { + $languageCode = $language->code(); + $languageTranslations = $language->translations(); - // injects languages translations - if ($languages = $this->languages()) { - foreach ($languages as $language) { - $languageCode = $language->code(); - $languageTranslations = $language->translations(); + // merges language translations with extensions translations + if (empty($languageTranslations) === false) { + $translations[$languageCode] = array_merge( + $translations[$languageCode] ?? [], + $languageTranslations + ); + } + } + } - // merges language translations with extensions translations - if (empty($languageTranslations) === false) { - $translations[$languageCode] = array_merge( - $translations[$languageCode] ?? [], - $languageTranslations - ); - } - } - } + $this->translations = Translations::load($this->root('i18n:translations'), $translations); - $this->translations = Translations::load($this->root('i18n:translations'), $translations); - - return $this->translations; - } + return $this->translations; + } } diff --git a/kirby/src/Cms/AppUsers.php b/kirby/src/Cms/AppUsers.php index 777dea9..8eedcce 100644 --- a/kirby/src/Cms/AppUsers.php +++ b/kirby/src/Cms/AppUsers.php @@ -16,128 +16,128 @@ use Throwable; */ trait AppUsers { - /** - * Cache for the auth auth layer - * - * @var Auth - */ - protected $auth; + /** + * Cache for the auth auth layer + * + * @var Auth + */ + protected $auth; - /** - * Returns the Authentication layer class - * - * @internal - * @return \Kirby\Cms\Auth - */ - public function auth() - { - return $this->auth = $this->auth ?? new Auth($this); - } + /** + * Returns the Authentication layer class + * + * @internal + * @return \Kirby\Cms\Auth + */ + public function auth() + { + return $this->auth = $this->auth ?? new Auth($this); + } - /** - * Become any existing user or disable the current user - * - * @param string|null $who User ID or email address, - * `null` to use the actual user again, - * `'kirby'` for a virtual admin user or - * `'nobody'` to disable the actual user - * @param Closure|null $callback Optional action function that will be run with - * the permissions of the impersonated user; the - * impersonation will be reset afterwards - * @return mixed If called without callback: User that was impersonated; - * if called with callback: Return value from the callback - * @throws \Throwable - */ - public function impersonate(?string $who = null, ?Closure $callback = null) - { - $auth = $this->auth(); + /** + * Become any existing user or disable the current user + * + * @param string|null $who User ID or email address, + * `null` to use the actual user again, + * `'kirby'` for a virtual admin user or + * `'nobody'` to disable the actual user + * @param Closure|null $callback Optional action function that will be run with + * the permissions of the impersonated user; the + * impersonation will be reset afterwards + * @return mixed If called without callback: User that was impersonated; + * if called with callback: Return value from the callback + * @throws \Throwable + */ + public function impersonate(?string $who = null, ?Closure $callback = null) + { + $auth = $this->auth(); - $userBefore = $auth->currentUserFromImpersonation(); - $userAfter = $auth->impersonate($who); + $userBefore = $auth->currentUserFromImpersonation(); + $userAfter = $auth->impersonate($who); - if ($callback === null) { - return $userAfter; - } + if ($callback === null) { + return $userAfter; + } - try { - // bind the App object to the callback - return $callback->call($this, $userAfter); - } catch (Throwable $e) { - throw $e; - } finally { - // ensure that the impersonation is *always* reset - // to the original value, even if an error occurred - $auth->impersonate($userBefore !== null ? $userBefore->id() : null); - } - } + try { + // bind the App object to the callback + return $callback->call($this, $userAfter); + } catch (Throwable $e) { + throw $e; + } finally { + // ensure that the impersonation is *always* reset + // to the original value, even if an error occurred + $auth->impersonate($userBefore !== null ? $userBefore->id() : null); + } + } - /** - * Set the currently active user id - * - * @param \Kirby\Cms\User|string $user - * @return \Kirby\Cms\App - */ - protected function setUser($user = null) - { - $this->user = $user; - return $this; - } + /** + * Set the currently active user id + * + * @param \Kirby\Cms\User|string $user + * @return \Kirby\Cms\App + */ + protected function setUser($user = null) + { + $this->user = $user; + return $this; + } - /** - * Create your own set of app users - * - * @param array|null $users - * @return \Kirby\Cms\App - */ - protected function setUsers(array $users = null) - { - if ($users !== null) { - $this->users = Users::factory($users, [ - 'kirby' => $this - ]); - } + /** + * Create your own set of app users + * + * @param array|null $users + * @return \Kirby\Cms\App + */ + protected function setUsers(array $users = null) + { + if ($users !== null) { + $this->users = Users::factory($users, [ + 'kirby' => $this + ]); + } - return $this; - } + return $this; + } - /** - * Returns a specific user by id - * or the current user if no id is given - * - * @param string|null $id - * @param bool $allowImpersonation If set to false, only the actually - * logged in user will be returned - * (when `$id` is passed as `null`) - * @return \Kirby\Cms\User|null - */ - public function user(?string $id = null, bool $allowImpersonation = true) - { - if ($id !== null) { - return $this->users()->find($id); - } + /** + * Returns a specific user by id + * or the current user if no id is given + * + * @param string|null $id + * @param bool $allowImpersonation If set to false, only the actually + * logged in user will be returned + * (when `$id` is passed as `null`) + * @return \Kirby\Cms\User|null + */ + public function user(?string $id = null, bool $allowImpersonation = true) + { + if ($id !== null) { + return $this->users()->find($id); + } - if ($allowImpersonation === true && is_string($this->user) === true) { - return $this->auth()->impersonate($this->user); - } else { - try { - return $this->auth()->user(null, $allowImpersonation); - } catch (Throwable $e) { - return null; - } - } - } + if ($allowImpersonation === true && is_string($this->user) === true) { + return $this->auth()->impersonate($this->user); + } else { + try { + return $this->auth()->user(null, $allowImpersonation); + } catch (Throwable $e) { + return null; + } + } + } - /** - * Returns all users - * - * @return \Kirby\Cms\Users - */ - public function users() - { - if (is_a($this->users, 'Kirby\Cms\Users') === true) { - return $this->users; - } + /** + * Returns all users + * + * @return \Kirby\Cms\Users + */ + public function users() + { + if (is_a($this->users, 'Kirby\Cms\Users') === true) { + return $this->users; + } - return $this->users = Users::load($this->root('accounts'), ['kirby' => $this]); - } + return $this->users = Users::load($this->root('accounts'), ['kirby' => $this]); + } } diff --git a/kirby/src/Cms/Auth.php b/kirby/src/Cms/Auth.php index ea903a6..6c81392 100644 --- a/kirby/src/Cms/Auth.php +++ b/kirby/src/Cms/Auth.php @@ -25,863 +25,864 @@ use Throwable; */ class Auth { - /** - * Available auth challenge classes - * from the core and plugins - * - * @var array - */ - public static $challenges = []; - - /** - * Currently impersonated user - * - * @var \Kirby\Cms\User|null - */ - protected $impersonate; - - /** - * Kirby instance - * - * @var \Kirby\Cms\App - */ - protected $kirby; - - /** - * Cache of the auth status object - * - * @var \Kirby\Cms\Auth\Status - */ - protected $status; - - /** - * Instance of the currently logged in user or - * `false` if the user was not yet determined - * - * @var \Kirby\Cms\User|null|false - */ - protected $user = false; - - /** - * Exception that was thrown while - * determining the current user - * - * @var \Throwable - */ - protected $userException; - - /** - * @param \Kirby\Cms\App $kirby - * @codeCoverageIgnore - */ - public function __construct(App $kirby) - { - $this->kirby = $kirby; - } - - /** - * Creates an authentication challenge - * (one-time auth code) - * @since 3.5.0 - * - * @param string $email - * @param bool $long If `true`, a long session will be created - * @param string $mode Either 'login' or 'password-reset' - * @return \Kirby\Cms\Auth\Status - * - * @throws \Kirby\Exception\LogicException If there is no suitable authentication challenge (only in debug mode) - * @throws \Kirby\Exception\NotFoundException If the user does not exist (only in debug mode) - * @throws \Kirby\Exception\PermissionException If the rate limit is exceeded - */ - public function createChallenge(string $email, bool $long = false, string $mode = 'login') - { - $email = $this->validateEmail($email); - - // rate-limit the number of challenges for DoS/DDoS protection - $this->track($email, false); - - $session = $this->kirby->session([ - 'createMode' => 'cookie', - 'long' => $long === true - ]); - - $challenge = null; - if ($user = $this->kirby->users()->find($email)) { - $timeout = $this->kirby->option('auth.challenge.timeout', 10 * 60); - - foreach ($this->enabledChallenges() as $name) { - $class = static::$challenges[$name] ?? null; - if ( - $class && - class_exists($class) === true && - is_subclass_of($class, 'Kirby\Cms\Auth\Challenge') === true && - $class::isAvailable($user, $mode) === true - ) { - $challenge = $name; - $code = $class::create($user, compact('mode', 'timeout')); - - $session->set('kirby.challenge.type', $challenge); - - if ($code !== null) { - $session->set('kirby.challenge.code', password_hash($code, PASSWORD_DEFAULT)); - $session->set('kirby.challenge.timeout', time() + $timeout); - } - - break; - } - } - - // if no suitable challenge was found, `$challenge === null` at this point; - // only leak this in debug mode - if ($challenge === null && $this->kirby->option('debug') === true) { - throw new LogicException('Could not find a suitable authentication challenge'); - } - } else { - $this->kirby->trigger('user.login:failed', compact('email')); - - // only leak the non-existing user in debug mode - if ($this->kirby->option('debug') === true) { - throw new NotFoundException([ - 'key' => 'user.notFound', - 'data' => [ - 'name' => $email - ] - ]); - } - } - - // always set the email, even if the challenge won't be - // created to avoid leaking whether the user exists - $session->set('kirby.challenge.email', $email); - - // sleep for a random amount of milliseconds - // to make automated attacks harder and to - // avoid leaking whether the user exists - usleep(random_int(1000, 300000)); - - // clear the status cache - $this->status = null; - - return $this->status($session, false); - } - - /** - * Returns the csrf token if it exists and if it is valid - * - * @return string|false - */ - public function csrf() - { - // get the csrf from the header - $fromHeader = $this->kirby->request()->csrf(); - - // check for a predefined csrf or use the one from session - $fromSession = $this->csrfFromSession(); - - // compare both tokens - if (hash_equals((string)$fromSession, (string)$fromHeader) !== true) { - return false; - } - - return $fromSession; - } - - /** - * Returns either predefined csrf or the one from session - * @since 3.6.0 - * - * @return string - */ - public function csrfFromSession(): string - { - $isDev = $this->kirby->option('panel.dev', false) !== false; - return $this->kirby->option('api.csrf', $isDev ? 'dev' : csrf()); - } - - /** - * Returns the logged in user by checking - * for a basic authentication header with - * valid credentials - * - * @param \Kirby\Http\Request\Auth\BasicAuth|null $auth - * @return \Kirby\Cms\User|null - * @throws \Kirby\Exception\InvalidArgumentException if the authorization header is invalid - * @throws \Kirby\Exception\PermissionException if basic authentication is not allowed - */ - public function currentUserFromBasicAuth(BasicAuth $auth = null) - { - if ($this->kirby->option('api.basicAuth', false) !== true) { - throw new PermissionException('Basic authentication is not activated'); - } - - // if logging in with password is disabled, basic auth cannot be possible either - $loginMethods = $this->kirby->system()->loginMethods(); - if (isset($loginMethods['password']) !== true) { - throw new PermissionException('Login with password is not enabled'); - } - - // if any login method requires 2FA, basic auth without 2FA would be a weakness - foreach ($loginMethods as $method) { - if (isset($method['2fa']) === true && $method['2fa'] === true) { - throw new PermissionException('Basic authentication cannot be used with 2FA'); - } - } - - $request = $this->kirby->request(); - $auth = $auth ?? $request->auth(); - - if (!$auth || $auth->type() !== 'basic') { - throw new InvalidArgumentException('Invalid authorization header'); - } - - // only allow basic auth when https is enabled or insecure requests permitted - if ($request->ssl() === false && $this->kirby->option('api.allowInsecure', false) !== true) { - throw new PermissionException('Basic authentication is only allowed over HTTPS'); - } - - return $this->validatePassword($auth->username(), $auth->password()); - } - - /** - * Returns the currently impersonated user - * - * @return \Kirby\Cms\User|null - */ - public function currentUserFromImpersonation() - { - return $this->impersonate; - } - - /** - * Returns the logged in user by checking - * the current session and finding a valid - * valid user id in there - * - * @param \Kirby\Session\Session|array|null $session - * @return \Kirby\Cms\User|null - */ - public function currentUserFromSession($session = null) - { - $session = $this->session($session); - - $id = $session->data()->get('kirby.userId'); - - if (is_string($id) !== true) { - return null; - } - - if ($user = $this->kirby->users()->find($id)) { - // in case the session needs to be updated, do it now - // for better performance - $session->commit(); - return $user; - } - - return null; - } - - /** - * Returns the list of enabled challenges in the - * configured order - * @since 3.5.1 - * - * @return array - */ - public function enabledChallenges(): array - { - return A::wrap($this->kirby->option('auth.challenges', ['email'])); - } - - /** - * Become any existing user or disable the current user - * - * @param string|null $who User ID or email address, - * `null` to use the actual user again, - * `'kirby'` for a virtual admin user or - * `'nobody'` to disable the actual user - * @return \Kirby\Cms\User|null - * @throws \Kirby\Exception\NotFoundException if the given user cannot be found - */ - public function impersonate(?string $who = null) - { - // clear the status cache - $this->status = null; - - switch ($who) { - case null: - return $this->impersonate = null; - case 'kirby': - return $this->impersonate = new User([ - 'email' => 'kirby@getkirby.com', - 'id' => 'kirby', - 'role' => 'admin', - ]); - case 'nobody': - return $this->impersonate = new User([ - 'email' => 'nobody@getkirby.com', - 'id' => 'nobody', - 'role' => 'nobody', - ]); - default: - if ($user = $this->kirby->users()->find($who)) { - return $this->impersonate = $user; - } - - throw new NotFoundException('The user "' . $who . '" cannot be found'); - } - } - - /** - * Returns the hashed ip of the visitor - * which is used to track invalid logins - * - * @return string - */ - public function ipHash(): string - { - $hash = hash('sha256', $this->kirby->visitor()->ip()); - - // only use the first 50 chars to ensure privacy - return substr($hash, 0, 50); - } - - /** - * Check if logins are blocked for the current ip or email - * - * @param string $email - * @return bool - */ - public function isBlocked(string $email): bool - { - $ip = $this->ipHash(); - $log = $this->log(); - $trials = $this->kirby->option('auth.trials', 10); - - if ($entry = ($log['by-ip'][$ip] ?? null)) { - if ($entry['trials'] >= $trials) { - return true; - } - } - - if ($this->kirby->users()->find($email)) { - if ($entry = ($log['by-email'][$email] ?? null)) { - if ($entry['trials'] >= $trials) { - return true; - } - } - } - - return false; - } - - /** - * Login a user by email and password - * - * @param string $email - * @param string $password - * @param bool $long - * @return \Kirby\Cms\User - * - * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off - * @throws \Kirby\Exception\NotFoundException If the email was invalid - * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) - */ - public function login(string $email, string $password, bool $long = false) - { - // session options - $options = [ - 'createMode' => 'cookie', - 'long' => $long === true - ]; - - // validate the user and log in to the session - $user = $this->validatePassword($email, $password); - $user->loginPasswordless($options); - - // clear the status cache - $this->status = null; - - return $user; - } - - /** - * Login a user by email, password and auth challenge - * @since 3.5.0 - * - * @param string $email - * @param string $password - * @param bool $long - * @return \Kirby\Cms\Auth\Status - * - * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off - * @throws \Kirby\Exception\NotFoundException If the email was invalid - * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) - */ - public function login2fa(string $email, string $password, bool $long = false) - { - $this->validatePassword($email, $password); - return $this->createChallenge($email, $long, '2fa'); - } - - /** - * Sets a user object as the current user in the cache - * @internal - * - * @param \Kirby\Cms\User $user - * @return void - */ - public function setUser(User $user): void - { - // stop impersonating - $this->impersonate = null; - - $this->user = $user; - - // clear the status cache - $this->status = null; - } - - /** - * Returns the authentication status object - * @since 3.5.1 - * - * @param \Kirby\Session\Session|array|null $session - * @param bool $allowImpersonation If set to false, only the actually - * logged in user will be returned - * @return \Kirby\Cms\Auth\Status - */ - public function status($session = null, bool $allowImpersonation = true) - { - // try to return from cache - if ($this->status && $session === null && $allowImpersonation === true) { - return $this->status; - } - - $sessionObj = $this->session($session); - - $props = ['kirby' => $this->kirby]; - if ($user = $this->user($sessionObj, $allowImpersonation)) { - // a user is currently logged in - if ($allowImpersonation === true && $this->impersonate !== null) { - $props['status'] = 'impersonated'; - } else { - $props['status'] = 'active'; - } - - $props['email'] = $user->email(); - } elseif ($email = $sessionObj->get('kirby.challenge.email')) { - // a challenge is currently pending - $props['status'] = 'pending'; - $props['email'] = $email; - $props['challenge'] = $sessionObj->get('kirby.challenge.type'); - $props['challengeFallback'] = A::last($this->enabledChallenges()); - } else { - // no active authentication - $props['status'] = 'inactive'; - } - - $status = new Status($props); - - // only cache the default object - if ($session === null && $allowImpersonation === true) { - $this->status = $status; - } - - return $status; - } - - /** - * Ensures that email addresses with IDN domains are in Unicode format - * and that the rate limit was not exceeded - * - * @param string $email - * @return string The normalized Unicode email address - * - * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded - */ - protected function validateEmail(string $email): string - { - // ensure that email addresses with IDN domains are in Unicode format - $email = Idn::decodeEmail($email); - - // check for blocked ips - if ($this->isBlocked($email) === true) { - $this->kirby->trigger('user.login:failed', compact('email')); - - if ($this->kirby->option('debug') === true) { - $message = 'Rate limit exceeded'; - } else { - // avoid leaking security-relevant information - $message = ['key' => 'access.login']; - } - - throw new PermissionException($message); - } - - return $email; - } - - /** - * Validates the user credentials and returns the user object on success; - * otherwise logs the failed attempt - * - * @param string $email - * @param string $password - * @return \Kirby\Cms\User - * - * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off - * @throws \Kirby\Exception\NotFoundException If the email was invalid - * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) - */ - public function validatePassword(string $email, string $password) - { - $email = $this->validateEmail($email); - - // validate the user - try { - if ($user = $this->kirby->users()->find($email)) { - if ($user->validatePassword($password) === true) { - return $user; - } - } - - throw new NotFoundException([ - 'key' => 'user.notFound', - 'data' => [ - 'name' => $email - ] - ]); - } catch (Throwable $e) { - // log invalid login trial - $this->track($email); - - // sleep for a random amount of milliseconds - // to make automated attacks harder - usleep(random_int(1000, 2000000)); - - // keep throwing the original error in debug mode, - // otherwise hide it to avoid leaking security-relevant information - if ($this->kirby->option('debug') === true) { - throw $e; - } else { - throw new PermissionException(['key' => 'access.login']); - } - } - } - - /** - * Returns the absolute path to the logins log - * - * @return string - */ - public function logfile(): string - { - return $this->kirby->root('accounts') . '/.logins'; - } - - /** - * Read all tracked logins - * - * @return array - */ - public function log(): array - { - try { - $log = Data::read($this->logfile(), 'json'); - $read = true; - } catch (Throwable $e) { - $log = []; - $read = false; - } - - // ensure that the category arrays are defined - $log['by-ip'] = $log['by-ip'] ?? []; - $log['by-email'] = $log['by-email'] ?? []; - - // remove all elements on the top level with different keys (old structure) - $log = array_intersect_key($log, array_flip(['by-ip', 'by-email'])); - - // remove entries that are no longer needed - $originalLog = $log; - $time = time() - $this->kirby->option('auth.timeout', 3600); - foreach ($log as $category => $entries) { - $log[$category] = array_filter( - $entries, - fn ($entry) => $entry['time'] > $time - ); - } - - // write new log to the file system if it changed - if ($read === false || $log !== $originalLog) { - if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) { - F::remove($this->logfile()); - } else { - Data::write($this->logfile(), $log, 'json'); - } - } - - return $log; - } - - /** - * Logout the current user - * - * @return void - */ - public function logout(): void - { - // stop impersonating; - // ensures that we log out the actually logged in user - $this->impersonate = null; - - // logout the current user if it exists - if ($user = $this->user()) { - $user->logout(); - } - - // clear the pending challenge - $session = $this->kirby->session(); - $session->remove('kirby.challenge.code'); - $session->remove('kirby.challenge.email'); - $session->remove('kirby.challenge.timeout'); - $session->remove('kirby.challenge.type'); - - // clear the status cache - $this->status = null; - } - - /** - * Clears the cached user data after logout - * @internal - * - * @return void - */ - public function flush(): void - { - $this->impersonate = null; - $this->status = null; - $this->user = null; - } - - /** - * Tracks a login - * - * @param string|null $email - * @param bool $triggerHook If `false`, no user.login:failed hook is triggered - * @return bool - */ - public function track(?string $email, bool $triggerHook = true): bool - { - if ($triggerHook === true) { - $this->kirby->trigger('user.login:failed', compact('email')); - } - - $ip = $this->ipHash(); - $log = $this->log(); - $time = time(); - - if (isset($log['by-ip'][$ip]) === true) { - $log['by-ip'][$ip] = [ - 'time' => $time, - 'trials' => ($log['by-ip'][$ip]['trials'] ?? 0) + 1 - ]; - } else { - $log['by-ip'][$ip] = [ - 'time' => $time, - 'trials' => 1 - ]; - } - - if ($email !== null && $this->kirby->users()->find($email)) { - if (isset($log['by-email'][$email]) === true) { - $log['by-email'][$email] = [ - 'time' => $time, - 'trials' => ($log['by-email'][$email]['trials'] ?? 0) + 1 - ]; - } else { - $log['by-email'][$email] = [ - 'time' => $time, - 'trials' => 1 - ]; - } - } - - return Data::write($this->logfile(), $log, 'json'); - } - - /** - * Returns the current authentication type - * - * @param bool $allowImpersonation If set to false, 'impersonate' won't - * be returned as authentication type - * even if an impersonation is active - * @return string - */ - public function type(bool $allowImpersonation = true): string - { - $basicAuth = $this->kirby->option('api.basicAuth', false); - $auth = $this->kirby->request()->auth(); - - if ($basicAuth === true && $auth && $auth->type() === 'basic') { - return 'basic'; - } elseif ($allowImpersonation === true && $this->impersonate !== null) { - return 'impersonate'; - } else { - return 'session'; - } - } - - /** - * Validates the currently logged in user - * - * @param \Kirby\Session\Session|array|null $session - * @param bool $allowImpersonation If set to false, only the actually - * logged in user will be returned - * @return \Kirby\Cms\User|null - * - * @throws \Throwable If an authentication error occurred - */ - public function user($session = null, bool $allowImpersonation = true) - { - if ($allowImpersonation === true && $this->impersonate !== null) { - return $this->impersonate; - } - - // return from cache - if ($this->user === null) { - // throw the same Exception again if one was captured before - if ($this->userException !== null) { - throw $this->userException; - } - - return null; - } elseif ($this->user !== false) { - return $this->user; - } - - try { - if ($this->type() === 'basic') { - return $this->user = $this->currentUserFromBasicAuth(); - } else { - return $this->user = $this->currentUserFromSession($session); - } - } catch (Throwable $e) { - $this->user = null; - - // capture the Exception for future calls - $this->userException = $e; - - throw $e; - } - } - - /** - * Verifies an authentication code that was - * requested with the `createChallenge()` method; - * if successful, the user is automatically logged in - * @since 3.5.0 - * - * @param string $code User-provided auth code to verify - * @return \Kirby\Cms\User User object of the logged-in user - * - * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded, the challenge timed out, the code - * is incorrect or if any other error occurred with debug mode off - * @throws \Kirby\Exception\NotFoundException If the user from the challenge doesn't exist - * @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active - * @throws \Kirby\Exception\LogicException If the authentication challenge is invalid - */ - public function verifyChallenge(string $code) - { - try { - $session = $this->kirby->session(); - - // first check if we have an active challenge at all - $email = $session->get('kirby.challenge.email'); - $challenge = $session->get('kirby.challenge.type'); - if (is_string($email) !== true || is_string($challenge) !== true) { - throw new InvalidArgumentException('No authentication challenge is active'); - } - - $user = $this->kirby->users()->find($email); - if ($user === null) { - throw new NotFoundException([ - 'key' => 'user.notFound', - 'data' => [ - 'name' => $email - ] - ]); - } - - // rate-limiting - if ($this->isBlocked($email) === true) { - $this->kirby->trigger('user.login:failed', compact('email')); - throw new PermissionException('Rate limit exceeded'); - } - - // time-limiting - $timeout = $session->get('kirby.challenge.timeout'); - if ($timeout !== null && time() > $timeout) { - throw new PermissionException('Authentication challenge timeout'); - } - - if ( - isset(static::$challenges[$challenge]) === true && - class_exists(static::$challenges[$challenge]) === true && - is_subclass_of(static::$challenges[$challenge], 'Kirby\Cms\Auth\Challenge') === true - ) { - $class = static::$challenges[$challenge]; - if ($class::verify($user, $code) === true) { - $this->logout(); - $user->loginPasswordless(); - - // clear the status cache - $this->status = null; - - return $user; - } else { - throw new PermissionException(['key' => 'access.code']); - } - } - - throw new LogicException('Invalid authentication challenge: ' . $challenge); - } catch (Throwable $e) { - if (empty($email) === false && $e->getMessage() !== 'Rate limit exceeded') { - $this->track($email); - } - - // sleep for a random amount of milliseconds - // to make automated attacks harder and to - // avoid leaking whether the user exists - usleep(random_int(1000, 2000000)); - - // keep throwing the original error in debug mode, - // otherwise hide it to avoid leaking security-relevant information - if ($this->kirby->option('debug') === true) { - throw $e; - } else { - throw new PermissionException(['key' => 'access.code']); - } - } - } - - /** - * Creates a session object from the passed options - * - * @param \Kirby\Session\Session|array|null $session - * @return \Kirby\Session\Session - */ - protected function session($session = null) - { - // use passed session options or session object if set - if (is_array($session) === true) { - return $this->kirby->session($session); - } - - // try session in header or cookie - if (is_a($session, 'Kirby\Session\Session') === false) { - return $this->kirby->session(['detect' => true]); - } - - return $session; - } + /** + * Available auth challenge classes + * from the core and plugins + * + * @var array + */ + public static $challenges = []; + + /** + * Currently impersonated user + * + * @var \Kirby\Cms\User|null + */ + protected $impersonate; + + /** + * Kirby instance + * + * @var \Kirby\Cms\App + */ + protected $kirby; + + /** + * Cache of the auth status object + * + * @var \Kirby\Cms\Auth\Status + */ + protected $status; + + /** + * Instance of the currently logged in user or + * `false` if the user was not yet determined + * + * @var \Kirby\Cms\User|null|false + */ + protected $user = false; + + /** + * Exception that was thrown while + * determining the current user + * + * @var \Throwable + */ + protected $userException; + + /** + * @param \Kirby\Cms\App $kirby + * @codeCoverageIgnore + */ + public function __construct(App $kirby) + { + $this->kirby = $kirby; + } + + /** + * Creates an authentication challenge + * (one-time auth code) + * @since 3.5.0 + * + * @param string $email + * @param bool $long If `true`, a long session will be created + * @param string $mode Either 'login' or 'password-reset' + * @return \Kirby\Cms\Auth\Status + * + * @throws \Kirby\Exception\LogicException If there is no suitable authentication challenge (only in debug mode) + * @throws \Kirby\Exception\NotFoundException If the user does not exist (only in debug mode) + * @throws \Kirby\Exception\PermissionException If the rate limit is exceeded + */ + public function createChallenge(string $email, bool $long = false, string $mode = 'login') + { + $email = $this->validateEmail($email); + + // rate-limit the number of challenges for DoS/DDoS protection + $this->track($email, false); + + $session = $this->kirby->session([ + 'createMode' => 'cookie', + 'long' => $long === true + ]); + + $challenge = null; + if ($user = $this->kirby->users()->find($email)) { + $timeout = $this->kirby->option('auth.challenge.timeout', 10 * 60); + + foreach ($this->enabledChallenges() as $name) { + $class = static::$challenges[$name] ?? null; + if ( + $class && + class_exists($class) === true && + is_subclass_of($class, 'Kirby\Cms\Auth\Challenge') === true && + $class::isAvailable($user, $mode) === true + ) { + $challenge = $name; + $code = $class::create($user, compact('mode', 'timeout')); + + $session->set('kirby.challenge.type', $challenge); + + if ($code !== null) { + $session->set('kirby.challenge.code', password_hash($code, PASSWORD_DEFAULT)); + $session->set('kirby.challenge.timeout', time() + $timeout); + } + + break; + } + } + + // if no suitable challenge was found, `$challenge === null` at this point; + // only leak this in debug mode + if ($challenge === null && $this->kirby->option('debug') === true) { + throw new LogicException('Could not find a suitable authentication challenge'); + } + } else { + $this->kirby->trigger('user.login:failed', compact('email')); + + // only leak the non-existing user in debug mode + if ($this->kirby->option('debug') === true) { + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $email + ] + ]); + } + } + + // always set the email, even if the challenge won't be + // created to avoid leaking whether the user exists + $session->set('kirby.challenge.email', $email); + + // sleep for a random amount of milliseconds + // to make automated attacks harder and to + // avoid leaking whether the user exists + usleep(random_int(1000, 300000)); + + // clear the status cache + $this->status = null; + + return $this->status($session, false); + } + + /** + * Returns the csrf token if it exists and if it is valid + * + * @return string|false + */ + public function csrf() + { + // get the csrf from the header + $fromHeader = $this->kirby->request()->csrf(); + + // check for a predefined csrf or use the one from session + $fromSession = $this->csrfFromSession(); + + // compare both tokens + if (hash_equals((string)$fromSession, (string)$fromHeader) !== true) { + return false; + } + + return $fromSession; + } + + /** + * Returns either predefined csrf or the one from session + * @since 3.6.0 + * + * @return string + */ + public function csrfFromSession(): string + { + $isDev = $this->kirby->option('panel.dev', false) !== false; + $fallback = $isDev ? 'dev' : $this->kirby->csrf(); + return $this->kirby->option('api.csrf', $fallback); + } + + /** + * Returns the logged in user by checking + * for a basic authentication header with + * valid credentials + * + * @param \Kirby\Http\Request\Auth\BasicAuth|null $auth + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\InvalidArgumentException if the authorization header is invalid + * @throws \Kirby\Exception\PermissionException if basic authentication is not allowed + */ + public function currentUserFromBasicAuth(BasicAuth $auth = null) + { + if ($this->kirby->option('api.basicAuth', false) !== true) { + throw new PermissionException('Basic authentication is not activated'); + } + + // if logging in with password is disabled, basic auth cannot be possible either + $loginMethods = $this->kirby->system()->loginMethods(); + if (isset($loginMethods['password']) !== true) { + throw new PermissionException('Login with password is not enabled'); + } + + // if any login method requires 2FA, basic auth without 2FA would be a weakness + foreach ($loginMethods as $method) { + if (isset($method['2fa']) === true && $method['2fa'] === true) { + throw new PermissionException('Basic authentication cannot be used with 2FA'); + } + } + + $request = $this->kirby->request(); + $auth = $auth ?? $request->auth(); + + if (!$auth || $auth->type() !== 'basic') { + throw new InvalidArgumentException('Invalid authorization header'); + } + + // only allow basic auth when https is enabled or insecure requests permitted + if ($request->ssl() === false && $this->kirby->option('api.allowInsecure', false) !== true) { + throw new PermissionException('Basic authentication is only allowed over HTTPS'); + } + + return $this->validatePassword($auth->username(), $auth->password()); + } + + /** + * Returns the currently impersonated user + * + * @return \Kirby\Cms\User|null + */ + public function currentUserFromImpersonation() + { + return $this->impersonate; + } + + /** + * Returns the logged in user by checking + * the current session and finding a valid + * valid user id in there + * + * @param \Kirby\Session\Session|array|null $session + * @return \Kirby\Cms\User|null + */ + public function currentUserFromSession($session = null) + { + $session = $this->session($session); + + $id = $session->data()->get('kirby.userId'); + + if (is_string($id) !== true) { + return null; + } + + if ($user = $this->kirby->users()->find($id)) { + // in case the session needs to be updated, do it now + // for better performance + $session->commit(); + return $user; + } + + return null; + } + + /** + * Returns the list of enabled challenges in the + * configured order + * @since 3.5.1 + * + * @return array + */ + public function enabledChallenges(): array + { + return A::wrap($this->kirby->option('auth.challenges', ['email'])); + } + + /** + * Become any existing user or disable the current user + * + * @param string|null $who User ID or email address, + * `null` to use the actual user again, + * `'kirby'` for a virtual admin user or + * `'nobody'` to disable the actual user + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\NotFoundException if the given user cannot be found + */ + public function impersonate(?string $who = null) + { + // clear the status cache + $this->status = null; + + switch ($who) { + case null: + return $this->impersonate = null; + case 'kirby': + return $this->impersonate = new User([ + 'email' => 'kirby@getkirby.com', + 'id' => 'kirby', + 'role' => 'admin', + ]); + case 'nobody': + return $this->impersonate = new User([ + 'email' => 'nobody@getkirby.com', + 'id' => 'nobody', + 'role' => 'nobody', + ]); + default: + if ($user = $this->kirby->users()->find($who)) { + return $this->impersonate = $user; + } + + throw new NotFoundException('The user "' . $who . '" cannot be found'); + } + } + + /** + * Returns the hashed ip of the visitor + * which is used to track invalid logins + * + * @return string + */ + public function ipHash(): string + { + $hash = hash('sha256', $this->kirby->visitor()->ip()); + + // only use the first 50 chars to ensure privacy + return substr($hash, 0, 50); + } + + /** + * Check if logins are blocked for the current ip or email + * + * @param string $email + * @return bool + */ + public function isBlocked(string $email): bool + { + $ip = $this->ipHash(); + $log = $this->log(); + $trials = $this->kirby->option('auth.trials', 10); + + if ($entry = ($log['by-ip'][$ip] ?? null)) { + if ($entry['trials'] >= $trials) { + return true; + } + } + + if ($this->kirby->users()->find($email)) { + if ($entry = ($log['by-email'][$email] ?? null)) { + if ($entry['trials'] >= $trials) { + return true; + } + } + } + + return false; + } + + /** + * Login a user by email and password + * + * @param string $email + * @param string $password + * @param bool $long + * @return \Kirby\Cms\User + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off + * @throws \Kirby\Exception\NotFoundException If the email was invalid + * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) + */ + public function login(string $email, string $password, bool $long = false) + { + // session options + $options = [ + 'createMode' => 'cookie', + 'long' => $long === true + ]; + + // validate the user and log in to the session + $user = $this->validatePassword($email, $password); + $user->loginPasswordless($options); + + // clear the status cache + $this->status = null; + + return $user; + } + + /** + * Login a user by email, password and auth challenge + * @since 3.5.0 + * + * @param string $email + * @param string $password + * @param bool $long + * @return \Kirby\Cms\Auth\Status + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off + * @throws \Kirby\Exception\NotFoundException If the email was invalid + * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) + */ + public function login2fa(string $email, string $password, bool $long = false) + { + $this->validatePassword($email, $password); + return $this->createChallenge($email, $long, '2fa'); + } + + /** + * Sets a user object as the current user in the cache + * @internal + * + * @param \Kirby\Cms\User $user + * @return void + */ + public function setUser(User $user): void + { + // stop impersonating + $this->impersonate = null; + + $this->user = $user; + + // clear the status cache + $this->status = null; + } + + /** + * Returns the authentication status object + * @since 3.5.1 + * + * @param \Kirby\Session\Session|array|null $session + * @param bool $allowImpersonation If set to false, only the actually + * logged in user will be returned + * @return \Kirby\Cms\Auth\Status + */ + public function status($session = null, bool $allowImpersonation = true) + { + // try to return from cache + if ($this->status && $session === null && $allowImpersonation === true) { + return $this->status; + } + + $sessionObj = $this->session($session); + + $props = ['kirby' => $this->kirby]; + if ($user = $this->user($sessionObj, $allowImpersonation)) { + // a user is currently logged in + if ($allowImpersonation === true && $this->impersonate !== null) { + $props['status'] = 'impersonated'; + } else { + $props['status'] = 'active'; + } + + $props['email'] = $user->email(); + } elseif ($email = $sessionObj->get('kirby.challenge.email')) { + // a challenge is currently pending + $props['status'] = 'pending'; + $props['email'] = $email; + $props['challenge'] = $sessionObj->get('kirby.challenge.type'); + $props['challengeFallback'] = A::last($this->enabledChallenges()); + } else { + // no active authentication + $props['status'] = 'inactive'; + } + + $status = new Status($props); + + // only cache the default object + if ($session === null && $allowImpersonation === true) { + $this->status = $status; + } + + return $status; + } + + /** + * Ensures that email addresses with IDN domains are in Unicode format + * and that the rate limit was not exceeded + * + * @param string $email + * @return string The normalized Unicode email address + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded + */ + protected function validateEmail(string $email): string + { + // ensure that email addresses with IDN domains are in Unicode format + $email = Idn::decodeEmail($email); + + // check for blocked ips + if ($this->isBlocked($email) === true) { + $this->kirby->trigger('user.login:failed', compact('email')); + + if ($this->kirby->option('debug') === true) { + $message = 'Rate limit exceeded'; + } else { + // avoid leaking security-relevant information + $message = ['key' => 'access.login']; + } + + throw new PermissionException($message); + } + + return $email; + } + + /** + * Validates the user credentials and returns the user object on success; + * otherwise logs the failed attempt + * + * @param string $email + * @param string $password + * @return \Kirby\Cms\User + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded or if any other error occurred with debug mode off + * @throws \Kirby\Exception\NotFoundException If the email was invalid + * @throws \Kirby\Exception\InvalidArgumentException If the password is not valid (via `$user->login()`) + */ + public function validatePassword(string $email, string $password) + { + $email = $this->validateEmail($email); + + // validate the user + try { + if ($user = $this->kirby->users()->find($email)) { + if ($user->validatePassword($password) === true) { + return $user; + } + } + + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $email + ] + ]); + } catch (Throwable $e) { + // log invalid login trial + $this->track($email); + + // sleep for a random amount of milliseconds + // to make automated attacks harder + usleep(random_int(1000, 2000000)); + + // keep throwing the original error in debug mode, + // otherwise hide it to avoid leaking security-relevant information + if ($this->kirby->option('debug') === true) { + throw $e; + } else { + throw new PermissionException(['key' => 'access.login']); + } + } + } + + /** + * Returns the absolute path to the logins log + * + * @return string + */ + public function logfile(): string + { + return $this->kirby->root('accounts') . '/.logins'; + } + + /** + * Read all tracked logins + * + * @return array + */ + public function log(): array + { + try { + $log = Data::read($this->logfile(), 'json'); + $read = true; + } catch (Throwable $e) { + $log = []; + $read = false; + } + + // ensure that the category arrays are defined + $log['by-ip'] = $log['by-ip'] ?? []; + $log['by-email'] = $log['by-email'] ?? []; + + // remove all elements on the top level with different keys (old structure) + $log = array_intersect_key($log, array_flip(['by-ip', 'by-email'])); + + // remove entries that are no longer needed + $originalLog = $log; + $time = time() - $this->kirby->option('auth.timeout', 3600); + foreach ($log as $category => $entries) { + $log[$category] = array_filter( + $entries, + fn ($entry) => $entry['time'] > $time + ); + } + + // write new log to the file system if it changed + if ($read === false || $log !== $originalLog) { + if (count($log['by-ip']) === 0 && count($log['by-email']) === 0) { + F::remove($this->logfile()); + } else { + Data::write($this->logfile(), $log, 'json'); + } + } + + return $log; + } + + /** + * Logout the current user + * + * @return void + */ + public function logout(): void + { + // stop impersonating; + // ensures that we log out the actually logged in user + $this->impersonate = null; + + // logout the current user if it exists + if ($user = $this->user()) { + $user->logout(); + } + + // clear the pending challenge + $session = $this->kirby->session(); + $session->remove('kirby.challenge.code'); + $session->remove('kirby.challenge.email'); + $session->remove('kirby.challenge.timeout'); + $session->remove('kirby.challenge.type'); + + // clear the status cache + $this->status = null; + } + + /** + * Clears the cached user data after logout + * @internal + * + * @return void + */ + public function flush(): void + { + $this->impersonate = null; + $this->status = null; + $this->user = null; + } + + /** + * Tracks a login + * + * @param string|null $email + * @param bool $triggerHook If `false`, no user.login:failed hook is triggered + * @return bool + */ + public function track(?string $email, bool $triggerHook = true): bool + { + if ($triggerHook === true) { + $this->kirby->trigger('user.login:failed', compact('email')); + } + + $ip = $this->ipHash(); + $log = $this->log(); + $time = time(); + + if (isset($log['by-ip'][$ip]) === true) { + $log['by-ip'][$ip] = [ + 'time' => $time, + 'trials' => ($log['by-ip'][$ip]['trials'] ?? 0) + 1 + ]; + } else { + $log['by-ip'][$ip] = [ + 'time' => $time, + 'trials' => 1 + ]; + } + + if ($email !== null && $this->kirby->users()->find($email)) { + if (isset($log['by-email'][$email]) === true) { + $log['by-email'][$email] = [ + 'time' => $time, + 'trials' => ($log['by-email'][$email]['trials'] ?? 0) + 1 + ]; + } else { + $log['by-email'][$email] = [ + 'time' => $time, + 'trials' => 1 + ]; + } + } + + return Data::write($this->logfile(), $log, 'json'); + } + + /** + * Returns the current authentication type + * + * @param bool $allowImpersonation If set to false, 'impersonate' won't + * be returned as authentication type + * even if an impersonation is active + * @return string + */ + public function type(bool $allowImpersonation = true): string + { + $basicAuth = $this->kirby->option('api.basicAuth', false); + $auth = $this->kirby->request()->auth(); + + if ($basicAuth === true && $auth && $auth->type() === 'basic') { + return 'basic'; + } elseif ($allowImpersonation === true && $this->impersonate !== null) { + return 'impersonate'; + } else { + return 'session'; + } + } + + /** + * Validates the currently logged in user + * + * @param \Kirby\Session\Session|array|null $session + * @param bool $allowImpersonation If set to false, only the actually + * logged in user will be returned + * @return \Kirby\Cms\User|null + * + * @throws \Throwable If an authentication error occurred + */ + public function user($session = null, bool $allowImpersonation = true) + { + if ($allowImpersonation === true && $this->impersonate !== null) { + return $this->impersonate; + } + + // return from cache + if ($this->user === null) { + // throw the same Exception again if one was captured before + if ($this->userException !== null) { + throw $this->userException; + } + + return null; + } elseif ($this->user !== false) { + return $this->user; + } + + try { + if ($this->type() === 'basic') { + return $this->user = $this->currentUserFromBasicAuth(); + } else { + return $this->user = $this->currentUserFromSession($session); + } + } catch (Throwable $e) { + $this->user = null; + + // capture the Exception for future calls + $this->userException = $e; + + throw $e; + } + } + + /** + * Verifies an authentication code that was + * requested with the `createChallenge()` method; + * if successful, the user is automatically logged in + * @since 3.5.0 + * + * @param string $code User-provided auth code to verify + * @return \Kirby\Cms\User User object of the logged-in user + * + * @throws \Kirby\Exception\PermissionException If the rate limit was exceeded, the challenge timed out, the code + * is incorrect or if any other error occurred with debug mode off + * @throws \Kirby\Exception\NotFoundException If the user from the challenge doesn't exist + * @throws \Kirby\Exception\InvalidArgumentException If no authentication challenge is active + * @throws \Kirby\Exception\LogicException If the authentication challenge is invalid + */ + public function verifyChallenge(string $code) + { + try { + $session = $this->kirby->session(); + + // first check if we have an active challenge at all + $email = $session->get('kirby.challenge.email'); + $challenge = $session->get('kirby.challenge.type'); + if (is_string($email) !== true || is_string($challenge) !== true) { + throw new InvalidArgumentException('No authentication challenge is active'); + } + + $user = $this->kirby->users()->find($email); + if ($user === null) { + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $email + ] + ]); + } + + // rate-limiting + if ($this->isBlocked($email) === true) { + $this->kirby->trigger('user.login:failed', compact('email')); + throw new PermissionException('Rate limit exceeded'); + } + + // time-limiting + $timeout = $session->get('kirby.challenge.timeout'); + if ($timeout !== null && time() > $timeout) { + throw new PermissionException('Authentication challenge timeout'); + } + + if ( + isset(static::$challenges[$challenge]) === true && + class_exists(static::$challenges[$challenge]) === true && + is_subclass_of(static::$challenges[$challenge], 'Kirby\Cms\Auth\Challenge') === true + ) { + $class = static::$challenges[$challenge]; + if ($class::verify($user, $code) === true) { + $this->logout(); + $user->loginPasswordless(); + + // clear the status cache + $this->status = null; + + return $user; + } else { + throw new PermissionException(['key' => 'access.code']); + } + } + + throw new LogicException('Invalid authentication challenge: ' . $challenge); + } catch (Throwable $e) { + if (empty($email) === false && $e->getMessage() !== 'Rate limit exceeded') { + $this->track($email); + } + + // sleep for a random amount of milliseconds + // to make automated attacks harder and to + // avoid leaking whether the user exists + usleep(random_int(1000, 2000000)); + + // keep throwing the original error in debug mode, + // otherwise hide it to avoid leaking security-relevant information + if ($this->kirby->option('debug') === true) { + throw $e; + } else { + throw new PermissionException(['key' => 'access.code']); + } + } + } + + /** + * Creates a session object from the passed options + * + * @param \Kirby\Session\Session|array|null $session + * @return \Kirby\Session\Session + */ + protected function session($session = null) + { + // use passed session options or session object if set + if (is_array($session) === true) { + return $this->kirby->session($session); + } + + // try session in header or cookie + if (is_a($session, 'Kirby\Session\Session') === false) { + return $this->kirby->session(['detect' => true]); + } + + return $session; + } } diff --git a/kirby/src/Cms/Auth/Challenge.php b/kirby/src/Cms/Auth/Challenge.php index 49cb59f..69a7574 100644 --- a/kirby/src/Cms/Auth/Challenge.php +++ b/kirby/src/Cms/Auth/Challenge.php @@ -16,48 +16,48 @@ use Kirby\Cms\User; */ abstract class Challenge { - /** - * Checks whether the challenge is available - * for the passed user and purpose - * - * @param \Kirby\Cms\User $user User the code will be generated for - * @param string $mode Purpose of the code ('login', 'reset' or '2fa') - * @return bool - */ - abstract public static function isAvailable(User $user, string $mode): bool; + /** + * Checks whether the challenge is available + * for the passed user and purpose + * + * @param \Kirby\Cms\User $user User the code will be generated for + * @param string $mode Purpose of the code ('login', 'reset' or '2fa') + * @return bool + */ + abstract public static function isAvailable(User $user, string $mode): bool; - /** - * Generates a random one-time auth code and returns that code - * for later verification - * - * @param \Kirby\Cms\User $user User to generate the code for - * @param array $options Details of the challenge request: - * - 'mode': Purpose of the code ('login', 'reset' or '2fa') - * - 'timeout': Number of seconds the code will be valid for - * @return string|null The generated and sent code or `null` in case - * there was no code to generate by this algorithm - */ - abstract public static function create(User $user, array $options): ?string; + /** + * Generates a random one-time auth code and returns that code + * for later verification + * + * @param \Kirby\Cms\User $user User to generate the code for + * @param array $options Details of the challenge request: + * - 'mode': Purpose of the code ('login', 'reset' or '2fa') + * - 'timeout': Number of seconds the code will be valid for + * @return string|null The generated and sent code or `null` in case + * there was no code to generate by this algorithm + */ + abstract public static function create(User $user, array $options): ?string; - /** - * Verifies the provided code against the created one; - * default implementation that checks the code that was - * returned from the `create()` method - * - * @param \Kirby\Cms\User $user User to check the code for - * @param string $code Code to verify - * @return bool - */ - public static function verify(User $user, string $code): bool - { - $hash = $user->kirby()->session()->get('kirby.challenge.code'); - if (is_string($hash) !== true) { - return false; - } + /** + * Verifies the provided code against the created one; + * default implementation that checks the code that was + * returned from the `create()` method + * + * @param \Kirby\Cms\User $user User to check the code for + * @param string $code Code to verify + * @return bool + */ + public static function verify(User $user, string $code): bool + { + $hash = $user->kirby()->session()->get('kirby.challenge.code'); + if (is_string($hash) !== true) { + return false; + } - // normalize the formatting in the user-provided code - $code = str_replace(' ', '', $code); + // normalize the formatting in the user-provided code + $code = str_replace(' ', '', $code); - return password_verify($code, $hash); - } + return password_verify($code, $hash); + } } diff --git a/kirby/src/Cms/Auth/EmailChallenge.php b/kirby/src/Cms/Auth/EmailChallenge.php index 6c6cbe1..e0bcfbe 100644 --- a/kirby/src/Cms/Auth/EmailChallenge.php +++ b/kirby/src/Cms/Auth/EmailChallenge.php @@ -18,60 +18,60 @@ use Kirby\Toolkit\Str; */ class EmailChallenge extends Challenge { - /** - * Checks whether the challenge is available - * for the passed user and purpose - * - * @param \Kirby\Cms\User $user User the code will be generated for - * @param string $mode Purpose of the code ('login', 'reset' or '2fa') - * @return bool - */ - public static function isAvailable(User $user, string $mode): bool - { - return true; - } + /** + * Checks whether the challenge is available + * for the passed user and purpose + * + * @param \Kirby\Cms\User $user User the code will be generated for + * @param string $mode Purpose of the code ('login', 'reset' or '2fa') + * @return bool + */ + public static function isAvailable(User $user, string $mode): bool + { + return true; + } - /** - * Generates a random one-time auth code and returns that code - * for later verification - * - * @param \Kirby\Cms\User $user User to generate the code for - * @param array $options Details of the challenge request: - * - 'mode': Purpose of the code ('login', 'reset' or '2fa') - * - 'timeout': Number of seconds the code will be valid for - * @return string The generated and sent code - */ - public static function create(User $user, array $options): string - { - $code = Str::random(6, 'num'); + /** + * Generates a random one-time auth code and returns that code + * for later verification + * + * @param \Kirby\Cms\User $user User to generate the code for + * @param array $options Details of the challenge request: + * - 'mode': Purpose of the code ('login', 'reset' or '2fa') + * - 'timeout': Number of seconds the code will be valid for + * @return string The generated and sent code + */ + public static function create(User $user, array $options): string + { + $code = Str::random(6, 'num'); - // insert a space in the middle for easier readability - $formatted = substr($code, 0, 3) . ' ' . substr($code, 3, 3); + // insert a space in the middle for easier readability + $formatted = substr($code, 0, 3) . ' ' . substr($code, 3, 3); - // use the login templates for 2FA - $mode = $options['mode']; - if ($mode === '2fa') { - $mode = 'login'; - } + // use the login templates for 2FA + $mode = $options['mode']; + if ($mode === '2fa') { + $mode = 'login'; + } - $kirby = $user->kirby(); - $kirby->email([ - 'from' => $kirby->option('auth.challenge.email.from', 'noreply@' . $kirby->url('index', true)->host()), - 'fromName' => $kirby->option('auth.challenge.email.fromName', $kirby->site()->title()), - 'to' => $user, - 'subject' => $kirby->option( - 'auth.challenge.email.subject', - I18n::translate('login.email.' . $mode . '.subject', null, $user->language()) - ), - 'template' => 'auth/' . $mode, - 'data' => [ - 'user' => $user, - 'site' => $kirby->system()->title(), - 'code' => $formatted, - 'timeout' => round($options['timeout'] / 60) - ] - ]); + $kirby = $user->kirby(); + $kirby->email([ + 'from' => $kirby->option('auth.challenge.email.from', 'noreply@' . $kirby->url('index', true)->host()), + 'fromName' => $kirby->option('auth.challenge.email.fromName', $kirby->site()->title()), + 'to' => $user, + 'subject' => $kirby->option( + 'auth.challenge.email.subject', + I18n::translate('login.email.' . $mode . '.subject', null, $user->language()) + ), + 'template' => 'auth/' . $mode, + 'data' => [ + 'user' => $user, + 'site' => $kirby->system()->title(), + 'code' => $formatted, + 'timeout' => round($options['timeout'] / 60) + ] + ]); - return $code; - } + return $code; + } } diff --git a/kirby/src/Cms/Auth/Status.php b/kirby/src/Cms/Auth/Status.php index 7716b8a..43031c7 100644 --- a/kirby/src/Cms/Auth/Status.php +++ b/kirby/src/Cms/Auth/Status.php @@ -19,201 +19,201 @@ use Kirby\Toolkit\Properties; */ class Status { - use Properties; + use Properties; - /** - * Type of the active challenge - * - * @var string|null - */ - protected $challenge = null; + /** + * Type of the active challenge + * + * @var string|null + */ + protected $challenge = null; - /** - * Challenge type to use as a fallback - * when $challenge is `null` - * - * @var string|null - */ - protected $challengeFallback = null; + /** + * Challenge type to use as a fallback + * when $challenge is `null` + * + * @var string|null + */ + protected $challengeFallback = null; - /** - * Email address of the current/pending user - * - * @var string|null - */ - protected $email = null; + /** + * Email address of the current/pending user + * + * @var string|null + */ + protected $email = null; - /** - * Kirby instance for user lookup - * - * @var \Kirby\Cms\App - */ - protected $kirby; + /** + * Kirby instance for user lookup + * + * @var \Kirby\Cms\App + */ + protected $kirby; - /** - * Authentication status: - * `active|impersonated|pending|inactive` - * - * @var string - */ - protected $status; + /** + * Authentication status: + * `active|impersonated|pending|inactive` + * + * @var string + */ + protected $status; - /** - * Class constructor - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setProperties($props); - } + /** + * Class constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } - /** - * Returns the authentication status - * - * @return string - */ - public function __toString(): string - { - return $this->status(); - } + /** + * Returns the authentication status + * + * @return string + */ + public function __toString(): string + { + return $this->status(); + } - /** - * Returns the type of the active challenge - * - * @param bool $automaticFallback If set to `false`, no faked challenge is returned; - * WARNING: never send the resulting `null` value to the - * user to avoid leaking whether the pending user exists - * @return string|null - */ - public function challenge(bool $automaticFallback = true): ?string - { - // never return a challenge type if the status doesn't match - if ($this->status() !== 'pending') { - return null; - } + /** + * Returns the type of the active challenge + * + * @param bool $automaticFallback If set to `false`, no faked challenge is returned; + * WARNING: never send the resulting `null` value to the + * user to avoid leaking whether the pending user exists + * @return string|null + */ + public function challenge(bool $automaticFallback = true): ?string + { + // never return a challenge type if the status doesn't match + if ($this->status() !== 'pending') { + return null; + } - if ($automaticFallback === false) { - return $this->challenge; - } else { - return $this->challenge ?? $this->challengeFallback; - } - } + if ($automaticFallback === false) { + return $this->challenge; + } else { + return $this->challenge ?? $this->challengeFallback; + } + } - /** - * Returns the email address of the current/pending user - * - * @return string|null - */ - public function email(): ?string - { - return $this->email; - } + /** + * Returns the email address of the current/pending user + * + * @return string|null + */ + public function email(): ?string + { + return $this->email; + } - /** - * Returns the authentication status - * - * @return string `active|impersonated|pending|inactive` - */ - public function status(): string - { - return $this->status; - } + /** + * Returns the authentication status + * + * @return string `active|impersonated|pending|inactive` + */ + public function status(): string + { + return $this->status; + } - /** - * Returns an array with all public status data - * - * @return array - */ - public function toArray(): array - { - return [ - 'challenge' => $this->challenge(), - 'email' => $this->email(), - 'status' => $this->status() - ]; - } + /** + * Returns an array with all public status data + * + * @return array + */ + public function toArray(): array + { + return [ + 'challenge' => $this->challenge(), + 'email' => $this->email(), + 'status' => $this->status() + ]; + } - /** - * Returns the currently logged in user - * - * @return \Kirby\Cms\User - */ - public function user() - { - // for security, only return the user if they are - // already logged in - if (in_array($this->status(), ['active', 'impersonated']) !== true) { - return null; - } + /** + * Returns the currently logged in user + * + * @return \Kirby\Cms\User + */ + public function user() + { + // for security, only return the user if they are + // already logged in + if (in_array($this->status(), ['active', 'impersonated']) !== true) { + return null; + } - return $this->kirby->user($this->email()); - } + return $this->kirby->user($this->email()); + } - /** - * Sets the type of the active challenge - * - * @param string|null $challenge - * @return $this - */ - protected function setChallenge(?string $challenge = null) - { - $this->challenge = $challenge; - return $this; - } + /** + * Sets the type of the active challenge + * + * @param string|null $challenge + * @return $this + */ + protected function setChallenge(?string $challenge = null) + { + $this->challenge = $challenge; + return $this; + } - /** - * Sets the challenge type to use as - * a fallback when $challenge is `null` - * - * @param string|null $challengeFallback - * @return $this - */ - protected function setChallengeFallback(?string $challengeFallback = null) - { - $this->challengeFallback = $challengeFallback; - return $this; - } + /** + * Sets the challenge type to use as + * a fallback when $challenge is `null` + * + * @param string|null $challengeFallback + * @return $this + */ + protected function setChallengeFallback(?string $challengeFallback = null) + { + $this->challengeFallback = $challengeFallback; + return $this; + } - /** - * Sets the email address of the current/pending user - * - * @param string|null $email - * @return $this - */ - protected function setEmail(?string $email = null) - { - $this->email = $email; - return $this; - } + /** + * Sets the email address of the current/pending user + * + * @param string|null $email + * @return $this + */ + protected function setEmail(?string $email = null) + { + $this->email = $email; + return $this; + } - /** - * Sets the Kirby instance for user lookup - * - * @param \Kirby\Cms\App $kirby - * @return $this - */ - protected function setKirby(App $kirby) - { - $this->kirby = $kirby; - return $this; - } + /** + * Sets the Kirby instance for user lookup + * + * @param \Kirby\Cms\App $kirby + * @return $this + */ + protected function setKirby(App $kirby) + { + $this->kirby = $kirby; + return $this; + } - /** - * Sets the authentication status - * - * @param string $status `active|impersonated|pending|inactive` - * @return $this - */ - protected function setStatus(string $status) - { - if (in_array($status, ['active', 'impersonated', 'pending', 'inactive']) !== true) { - throw new InvalidArgumentException([ - 'data' => ['argument' => '$props[\'status\']', 'method' => 'Status::__construct'] - ]); - } + /** + * Sets the authentication status + * + * @param string $status `active|impersonated|pending|inactive` + * @return $this + */ + protected function setStatus(string $status) + { + if (in_array($status, ['active', 'impersonated', 'pending', 'inactive']) !== true) { + throw new InvalidArgumentException([ + 'data' => ['argument' => '$props[\'status\']', 'method' => 'Status::__construct'] + ]); + } - $this->status = $status; - return $this; - } + $this->status = $status; + return $this; + } } diff --git a/kirby/src/Cms/Block.php b/kirby/src/Cms/Block.php index 3033897..97f04a7 100644 --- a/kirby/src/Cms/Block.php +++ b/kirby/src/Cms/Block.php @@ -20,267 +20,256 @@ use Throwable; */ class Block extends Item { - use HasMethods; + use HasMethods; - public const ITEMS_CLASS = '\Kirby\Cms\Blocks'; + public const ITEMS_CLASS = '\Kirby\Cms\Blocks'; - /** - * @var \Kirby\Cms\Content - */ - protected $content; + /** + * @var \Kirby\Cms\Content + */ + protected $content; - /** - * @var bool - */ - protected $isHidden; + /** + * @var bool + */ + protected $isHidden; - /** - * Registry with all block models - * - * @var array - */ - public static $models = []; + /** + * Registry with all block models + * + * @var array + */ + public static $models = []; - /** - * @var string - */ - protected $type; + /** + * @var string + */ + protected $type; - /** - * Proxy for content fields - * - * @param string $method - * @param array $args - * @return \Kirby\Cms\Field - */ - public function __call(string $method, array $args = []) - { - // block methods - if ($this->hasMethod($method)) { - return $this->callMethod($method, $args); - } + /** + * Proxy for content fields + * + * @param string $method + * @param array $args + * @return \Kirby\Cms\Field + */ + public function __call(string $method, array $args = []) + { + // block methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $args); + } - return $this->content()->get($method); - } + return $this->content()->get($method); + } - /** - * Creates a new block object - * - * @param array $params - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function __construct(array $params) - { - parent::__construct($params); + /** + * Creates a new block object + * + * @param array $params + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(array $params) + { + parent::__construct($params); - // import old builder format - $params = BlockConverter::builderBlock($params); - $params = BlockConverter::editorBlock($params); + if (isset($params['type']) === false) { + throw new InvalidArgumentException('The block type is missing'); + } - if (isset($params['type']) === false) { - throw new InvalidArgumentException('The block type is missing'); - } + // make sure the content is always defined as array to keep + // at least a bit of backward compatibility with older fields + if (is_array($params['content'] ?? null) === false) { + $params['content'] = []; + } - $this->content = $params['content'] ?? []; - $this->isHidden = $params['isHidden'] ?? false; - $this->type = $params['type']; + $this->content = $params['content']; + $this->isHidden = $params['isHidden'] ?? false; + $this->type = $params['type']; - // create the content object - $this->content = new Content($this->content, $this->parent); - } + // create the content object + $this->content = new Content($this->content, $this->parent); + } - /** - * Converts the object to a string - * - * @return string - */ - public function __toString(): string - { - return $this->toHtml(); - } + /** + * Converts the object to a string + * + * @return string + */ + public function __toString(): string + { + return $this->toHtml(); + } - /** - * Deprecated method to return the block type - * - * @deprecated 3.5.0 Use `\Kirby\Cms\Block::type()` instead - * @todo Remove in 3.7.0 - * - * @return string - */ - public function _key(): string - { - deprecated('Block::_key() has been deprecated. Use Block::type() instead.'); - return $this->type(); - } + /** + * Returns the content object + * + * @return \Kirby\Cms\Content + */ + public function content() + { + return $this->content; + } - /** - * Deprecated method to return the block id - * - * @deprecated 3.5.0 Use `\Kirby\Cms\Block::id()` instead - * @todo Remove in 3.7.0 - * - * @return string - */ - public function _uid(): string - { - deprecated('Block::_uid() has been deprecated. Use Block::id() instead.'); - return $this->id(); - } + /** + * Controller for the block snippet + * + * @return array + */ + public function controller(): array + { + return [ + 'block' => $this, + 'content' => $this->content(), + // deprecated block data + 'data' => $this, + 'id' => $this->id(), + 'prev' => $this->prev(), + 'next' => $this->next() + ]; + } - /** - * Returns the content object - * - * @return \Kirby\Cms\Content - */ - public function content() - { - return $this->content; - } + /** + * Converts the block to HTML and then + * uses the Str::excerpt method to create + * a non-formatted, shortened excerpt from it + * + * @param mixed ...$args + * @return string + */ + public function excerpt(...$args) + { + return Str::excerpt($this->toHtml(), ...$args); + } - /** - * Controller for the block snippet - * - * @return array - */ - public function controller(): array - { - return [ - 'block' => $this, - 'content' => $this->content(), - // deprecated block data - 'data' => $this, - 'id' => $this->id(), - 'prev' => $this->prev(), - 'next' => $this->next() - ]; - } + /** + * Constructs a block object with registering blocks models + * + * @param array $params + * @return static + * @throws \Kirby\Exception\InvalidArgumentException + * @internal + */ + public static function factory(array $params) + { + $type = $params['type'] ?? null; - /** - * Converts the block to HTML and then - * uses the Str::excerpt method to create - * a non-formatted, shortened excerpt from it - * - * @param mixed ...$args - * @return string - */ - public function excerpt(...$args) - { - return Str::excerpt($this->toHtml(), ...$args); - } + if (empty($type) === false && $class = (static::$models[$type] ?? null)) { + $object = new $class($params); - /** - * Constructs a block object with registering blocks models - * - * @param array $params - * @return static - * @throws \Kirby\Exception\InvalidArgumentException - * @internal - */ - public static function factory(array $params) - { - $type = $params['type'] ?? null; + if (is_a($object, 'Kirby\Cms\Block') === true) { + return $object; + } + } - if (empty($type) === false && $class = (static::$models[$type] ?? null)) { - $object = new $class($params); + // default model for blocks + if ($class = (static::$models['Kirby\Cms\Block'] ?? null)) { + $object = new $class($params); - if (is_a($object, 'Kirby\Cms\Block') === true) { - return $object; - } - } + if (is_a($object, 'Kirby\Cms\Block') === true) { + return $object; + } + } - // default model for blocks - if ($class = (static::$models['Kirby\Cms\Block'] ?? null)) { - $object = new $class($params); + return new static($params); + } - if (is_a($object, 'Kirby\Cms\Block') === true) { - return $object; - } - } + /** + * Checks if the block is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->content()->toArray()); + } - return new static($params); - } + /** + * Checks if the block is hidden + * from being rendered in the frontend + * + * @return bool + */ + public function isHidden(): bool + { + return $this->isHidden; + } - /** - * Checks if the block is empty - * - * @return bool - */ - public function isEmpty(): bool - { - return empty($this->content()->toArray()); - } + /** + * Checks if the block is not empty + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->isEmpty() === false; + } - /** - * Checks if the block is hidden - * from being rendered in the frontend - * - * @return bool - */ - public function isHidden(): bool - { - return $this->isHidden; - } + /** + * Returns the sibling collection that filtered by block status + * + * @return \Kirby\Cms\Collection + */ + protected function siblingsCollection() + { + return $this->siblings->filter('isHidden', $this->isHidden()); + } - /** - * Checks if the block is not empty - * - * @return bool - */ - public function isNotEmpty(): bool - { - return $this->isEmpty() === false; - } + /** + * Returns the block type + * + * @return string + */ + public function type(): string + { + return $this->type; + } - /** - * Returns the block type - * - * @return string - */ - public function type(): string - { - return $this->type; - } + /** + * The result is being sent to the editor + * via the API in the panel + * + * @return array + */ + public function toArray(): array + { + return [ + 'content' => $this->content()->toArray(), + 'id' => $this->id(), + 'isHidden' => $this->isHidden(), + 'type' => $this->type(), + ]; + } - /** - * The result is being sent to the editor - * via the API in the panel - * - * @return array - */ - public function toArray(): array - { - return [ - 'content' => $this->content()->toArray(), - 'id' => $this->id(), - 'isHidden' => $this->isHidden(), - 'type' => $this->type(), - ]; - } + /** + * Converts the block to html first + * and then places that inside a field + * object. This can be used further + * with all available field methods + * + * @return \Kirby\Cms\Field + */ + public function toField() + { + return new Field($this->parent(), $this->id(), $this->toHtml()); + } - /** - * Converts the block to html first - * and then places that inside a field - * object. This can be used further - * with all available field methods - * - * @return \Kirby\Cms\Field - */ - public function toField() - { - return new Field($this->parent(), $this->id(), $this->toHtml()); - } + /** + * Converts the block to HTML + * + * @return string + */ + public function toHtml(): string + { + try { + $kirby = $this->parent()->kirby(); + return (string)$kirby->snippet('blocks/' . $this->type(), $this->controller(), true); + } catch (Throwable $e) { + if ($kirby->option('debug') === true) { + return '

Block error: "' . $e->getMessage() . '" in block type: "' . $this->type() . '"

'; + } - /** - * Converts the block to HTML - * - * @return string - */ - public function toHtml(): string - { - try { - return (string)snippet('blocks/' . $this->type(), $this->controller(), true); - } catch (Throwable $e) { - return '

Block error: "' . $e->getMessage() . '" in block type: "' . $this->type() . '"

'; - } - } + return ''; + } + } } diff --git a/kirby/src/Cms/BlockConverter.php b/kirby/src/Cms/BlockConverter.php deleted file mode 100644 index 9939c8a..0000000 --- a/kirby/src/Cms/BlockConverter.php +++ /dev/null @@ -1,280 +0,0 @@ - - * @link https://getkirby.com - * @copyright Bastian Allgeier - * @license https://getkirby.com/license - */ -class BlockConverter -{ - public static function builderBlock(array $params): array - { - if (isset($params['_key']) === false) { - return $params; - } - - $params['type'] = $params['_key']; - $params['content'] = $params; - unset($params['_uid']); - - return $params; - } - - public static function editorBlock(array $params): array - { - if (static::isEditorBlock($params) === false) { - return $params; - } - - $method = 'editor' . $params['type']; - - if (method_exists(static::class, $method) === true) { - $params = static::$method($params); - } else { - $params = static::editorCustom($params); - } - - return $params; - } - - public static function editorBlocks(array $blocks = []): array - { - if (empty($blocks) === true) { - return $blocks; - } - - if (static::isEditorBlock($blocks[0]) === false) { - return $blocks; - } - - $list = []; - $listStart = null; - - foreach ($blocks as $index => $block) { - if (in_array($block['type'], ['ul', 'ol']) === true) { - $prev = $blocks[$index-1] ?? null; - $next = $blocks[$index+1] ?? null; - - // new list starts here - if (!$prev || $prev['type'] !== $block['type']) { - $listStart = $index; - } - - // add the block to the list - $list[] = $block; - - // list ends here - if (!$next || $next['type'] !== $block['type']) { - $blocks[$listStart] = [ - 'content' => [ - 'text' => - '<' . $block['type'] . '>' . - implode(array_map(function ($item) { - return '
  • ' . $item['content'] . '
  • '; - }, $list)) . - '', - ], - 'type' => 'list' - ]; - - $start = $listStart + 1; - $end = $listStart + count($list); - - for ($x = $start; $x <= $end; $x++) { - $blocks[$x] = false; - } - - $listStart = null; - $list = []; - } - } else { - $blocks[$index] = static::editorBlock($block); - } - } - - return array_filter($blocks); - } - - public static function editorBlockquote(array $params): array - { - return [ - 'content' => [ - 'text' => $params['content'] - ], - 'type' => 'quote' - ]; - } - - public static function editorCode(array $params): array - { - return [ - 'content' => [ - 'language' => $params['attrs']['language'] ?? null, - 'code' => $params['content'] - ], - 'type' => 'code' - ]; - } - - public static function editorCustom(array $params): array - { - return [ - 'content' => array_merge( - $params['attrs'] ?? [], - [ - 'body' => $params['content'] ?? null - ] - ), - 'type' => $params['type'] ?? 'unknown' - ]; - } - - public static function editorH1(array $params): array - { - return static::editorHeading($params, 'h1'); - } - - public static function editorH2(array $params): array - { - return static::editorHeading($params, 'h2'); - } - - public static function editorH3(array $params): array - { - return static::editorHeading($params, 'h3'); - } - - public static function editorH4(array $params): array - { - return static::editorHeading($params, 'h4'); - } - - public static function editorH5(array $params): array - { - return static::editorHeading($params, 'h5'); - } - - public static function editorH6(array $params): array - { - return static::editorHeading($params, 'h6'); - } - - public static function editorHr(array $params): array - { - return [ - 'content' => [], - 'type' => 'line' - ]; - } - - public static function editorHeading(array $params, string $level): array - { - return [ - 'content' => [ - 'level' => $level, - 'text' => $params['content'] - ], - 'type' => 'heading' - ]; - } - - public static function editorImage(array $params): array - { - // internal image - if (isset($params['attrs']['id']) === true) { - return [ - 'content' => [ - 'alt' => $params['attrs']['alt'] ?? null, - 'caption' => $params['attrs']['caption'] ?? null, - 'image' => $params['attrs']['id'] ?? $params['attrs']['src'] ?? null, - 'location' => 'kirby', - 'ratio' => $params['attrs']['ratio'] ?? null, - ], - 'type' => 'image' - ]; - } - - return [ - 'content' => [ - 'alt' => $params['attrs']['alt'] ?? null, - 'caption' => $params['attrs']['caption'] ?? null, - 'src' => $params['attrs']['src'] ?? null, - 'location' => 'web', - 'ratio' => $params['attrs']['ratio'] ?? null, - ], - 'type' => 'image' - ]; - } - - public static function editorKirbytext(array $params): array - { - return [ - 'content' => [ - 'text' => $params['content'] - ], - 'type' => 'markdown' - ]; - } - - public static function editorOl(array $params): array - { - return [ - 'content' => [ - 'text' => $params['content'] - ], - 'type' => 'list' - ]; - } - - public static function editorParagraph(array $params): array - { - return [ - 'content' => [ - 'text' => '

    ' . $params['content'] . '

    ' - ], - 'type' => 'text' - ]; - } - - public static function editorUl(array $params): array - { - return [ - 'content' => [ - 'text' => $params['content'] - ], - 'type' => 'list' - ]; - } - - public static function editorVideo(array $params): array - { - return [ - 'content' => [ - 'caption' => $params['attrs']['caption'] ?? null, - 'url' => $params['attrs']['src'] ?? null - ], - 'type' => 'video' - ]; - } - - public static function isEditorBlock(array $params): bool - { - if (isset($params['attrs']) === true) { - return true; - } - - if (is_string($params['content'] ?? null) === true) { - return true; - } - - return false; - } -} diff --git a/kirby/src/Cms/Blocks.php b/kirby/src/Cms/Blocks.php index 35f96c6..49589ec 100644 --- a/kirby/src/Cms/Blocks.php +++ b/kirby/src/Cms/Blocks.php @@ -2,12 +2,9 @@ namespace Kirby\Cms; -use Exception; use Kirby\Data\Json; -use Kirby\Data\Yaml; use Kirby\Parsley\Parsley; use Kirby\Parsley\Schema\Blocks as BlockSchema; -use Kirby\Toolkit\A; use Kirby\Toolkit\Str; use Throwable; @@ -23,144 +20,126 @@ use Throwable; */ class Blocks extends Items { - public const ITEM_CLASS = '\Kirby\Cms\Block'; + public const ITEM_CLASS = '\Kirby\Cms\Block'; - /** - * Return HTML when the collection is - * converted to a string - * - * @return string - */ - public function __toString(): string - { - return $this->toHtml(); - } + /** + * Return HTML when the collection is + * converted to a string + * + * @return string + */ + public function __toString(): string + { + return $this->toHtml(); + } - /** - * Converts the blocks to HTML and then - * uses the Str::excerpt method to create - * a non-formatted, shortened excerpt from it - * - * @param mixed ...$args - * @return string - */ - public function excerpt(...$args) - { - return Str::excerpt($this->toHtml(), ...$args); - } + /** + * Converts the blocks to HTML and then + * uses the Str::excerpt method to create + * a non-formatted, shortened excerpt from it + * + * @param mixed ...$args + * @return string + */ + public function excerpt(...$args) + { + return Str::excerpt($this->toHtml(), ...$args); + } - /** - * Wrapper around the factory to - * catch blocks from layouts - * - * @param array $items - * @param array $params - * @return \Kirby\Cms\Blocks - */ - public static function factory(array $items = null, array $params = []) - { - $items = static::extractFromLayouts($items); - $items = BlockConverter::editorBlocks($items); + /** + * Wrapper around the factory to + * catch blocks from layouts + * + * @param array $items + * @param array $params + * @return \Kirby\Cms\Blocks + */ + public static function factory(array $items = null, array $params = []) + { + $items = static::extractFromLayouts($items); - return parent::factory($items, $params); - } + return parent::factory($items, $params); + } - /** - * Pull out blocks from layouts - * - * @param array $input - * @return array - */ - protected static function extractFromLayouts(array $input): array - { - if (empty($input) === true) { - return []; - } + /** + * Pull out blocks from layouts + * + * @param array $input + * @return array + */ + protected static function extractFromLayouts(array $input): array + { + if (empty($input) === true) { + return []; + } - if ( - // no columns = no layout - array_key_exists('columns', $input[0]) === false || - // checks if this is a block for the builder plugin - array_key_exists('_key', $input[0]) === true - ) { - return $input; - } + // no columns = no layout + if (array_key_exists('columns', $input[0]) === false) { + return $input; + } - $blocks = []; + $blocks = []; - foreach ($input as $layout) { - foreach (($layout['columns'] ?? []) as $column) { - foreach (($column['blocks'] ?? []) as $block) { - $blocks[] = $block; - } - } - } + foreach ($input as $layout) { + foreach (($layout['columns'] ?? []) as $column) { + foreach (($column['blocks'] ?? []) as $block) { + $blocks[] = $block; + } + } + } - return $blocks; - } + return $blocks; + } - /** - * Checks if a given block type exists in the collection - * @since 3.6.0 - * - * @param string $type - * @return bool - */ - public function hasType(string $type): bool - { - return $this->filterBy('type', $type)->count() > 0; - } + /** + * Checks if a given block type exists in the collection + * @since 3.6.0 + * + * @param string $type + * @return bool + */ + public function hasType(string $type): bool + { + return $this->filterBy('type', $type)->count() > 0; + } - /** - * Parse and sanitize various block formats - * - * @param array|string $input - * @return array - */ - public static function parse($input): array - { - if (empty($input) === false && is_array($input) === false) { - try { - $input = Json::decode((string)$input); - } catch (Throwable $e) { - try { - // try to import the old YAML format - $yaml = Yaml::decode((string)$input); - $first = A::first($yaml); + /** + * Parse and sanitize various block formats + * + * @param array|string $input + * @return array + */ + public static function parse($input): array + { + if (empty($input) === false && is_array($input) === false) { + try { + $input = Json::decode((string)$input); + } catch (Throwable $e) { + $parser = new Parsley((string)$input, new BlockSchema()); + $input = $parser->blocks(); + } + } - // check for valid yaml - if (empty($yaml) === true || (isset($first['_key']) === false && isset($first['type']) === false)) { - throw new Exception('Invalid YAML'); - } else { - $input = $yaml; - } - } catch (Throwable $e) { - $parser = new Parsley((string)$input, new BlockSchema()); - $input = $parser->blocks(); - } - } - } + if (empty($input) === true) { + return []; + } - if (empty($input) === true) { - return []; - } + return $input; + } - return $input; - } + /** + * Convert all blocks to HTML + * + * @return string + */ + public function toHtml(): string + { + $html = []; - /** - * Convert all blocks to HTML - * - * @return string - */ - public function toHtml(): string - { - $html = []; + foreach ($this->data as $block) { + $html[] = $block->toHtml(); + } - foreach ($this->data as $block) { - $html[] = $block->toHtml(); - } - - return implode($html); - } + return implode($html); + } } diff --git a/kirby/src/Cms/Blueprint.php b/kirby/src/Cms/Blueprint.php index 75741d0..bd257c2 100644 --- a/kirby/src/Cms/Blueprint.php +++ b/kirby/src/Cms/Blueprint.php @@ -25,792 +25,794 @@ use Throwable; */ class Blueprint { - public static $presets = []; - public static $loaded = []; - - protected $fields = []; - protected $model; - protected $props; - protected $sections = []; - protected $tabs = []; - - /** - * Magic getter/caller for any blueprint prop - * - * @param string $key - * @param array|null $arguments - * @return mixed - */ - public function __call(string $key, array $arguments = null) - { - return $this->props[$key] ?? null; - } - - /** - * Creates a new blueprint object with the given props - * - * @param array $props - * @throws \Kirby\Exception\InvalidArgumentException If the blueprint model is missing - */ - public function __construct(array $props) - { - if (empty($props['model']) === true) { - throw new InvalidArgumentException('A blueprint model is required'); - } - - if (is_a($props['model'], ModelWithContent::class) === false) { - throw new InvalidArgumentException('Invalid blueprint model'); - } - - $this->model = $props['model']; - - // the model should not be included in the props array - unset($props['model']); - - // extend the blueprint in general - $props = $this->extend($props); - - // apply any blueprint preset - $props = $this->preset($props); - - // normalize the name - $props['name'] ??= 'default'; - - // normalize and translate the title - $props['title'] = $this->i18n($props['title'] ?? ucfirst($props['name'])); - - // convert all shortcuts - $props = $this->convertFieldsToSections('main', $props); - $props = $this->convertSectionsToColumns('main', $props); - $props = $this->convertColumnsToTabs('main', $props); - - // normalize all tabs - $props['tabs'] = $this->normalizeTabs($props['tabs'] ?? []); - - $this->props = $props; - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->props ?? []; - } - - /** - * Converts all column definitions, that - * are not wrapped in a tab, into a generic tab - * - * @param string $tabName - * @param array $props - * @return array - */ - protected function convertColumnsToTabs(string $tabName, array $props): array - { - if (isset($props['columns']) === false) { - return $props; - } - - // wrap everything in a main tab - $props['tabs'] = [ - $tabName => [ - 'columns' => $props['columns'] - ] - ]; - - unset($props['columns']); - - return $props; - } - - /** - * Converts all field definitions, that are not - * wrapped in a fields section into a generic - * fields section. - * - * @param string $tabName - * @param array $props - * @return array - */ - protected function convertFieldsToSections(string $tabName, array $props): array - { - if (isset($props['fields']) === false) { - return $props; - } - - // wrap all fields in a section - $props['sections'] = [ - $tabName . '-fields' => [ - 'type' => 'fields', - 'fields' => $props['fields'] - ] - ]; - - unset($props['fields']); - - return $props; - } - - /** - * Converts all sections that are not wrapped in - * columns, into a single generic column. - * - * @param string $tabName - * @param array $props - * @return array - */ - protected function convertSectionsToColumns(string $tabName, array $props): array - { - if (isset($props['sections']) === false) { - return $props; - } - - // wrap everything in one big column - $props['columns'] = [ - [ - 'width' => '1/1', - 'sections' => $props['sections'] - ] - ]; - - unset($props['sections']); - - return $props; - } - - /** - * Extends the props with props from a given - * mixin, when an extends key is set or the - * props is just a string - * - * @param array|string $props - * @return array - */ - public static function extend($props): array - { - if (is_string($props) === true) { - $props = [ - 'extends' => $props - ]; - } - - $extends = $props['extends'] ?? null; - - if ($extends === null) { - return $props; - } - - try { - $mixin = static::find($extends); - $mixin = static::extend($mixin); - $props = A::merge($mixin, $props, A::MERGE_REPLACE); - } catch (Exception $e) { - // keep the props unextended if the snippet wasn't found - } - - // remove the extends flag - unset($props['extends']); - - return $props; - } - - /** - * Create a new blueprint for a model - * - * @param string $name - * @param string|null $fallback - * @param \Kirby\Cms\Model $model - * @return static|null - */ - public static function factory(string $name, string $fallback = null, Model $model) - { - try { - $props = static::load($name); - } catch (Exception $e) { - $props = $fallback !== null ? static::load($fallback) : null; - } - - if ($props === null) { - return null; - } - - // inject the parent model - $props['model'] = $model; - - return new static($props); - } - - /** - * Returns a single field definition by name - * - * @param string $name - * @return array|null - */ - public function field(string $name): ?array - { - return $this->fields[$name] ?? null; - } - - /** - * Returns all field definitions - * - * @return array - */ - public function fields(): array - { - return $this->fields; - } - - /** - * Find a blueprint by name - * - * @param string $name - * @return array - * @throws \Kirby\Exception\NotFoundException If the blueprint cannot be found - */ - public static function find(string $name): array - { - if (isset(static::$loaded[$name]) === true) { - return static::$loaded[$name]; - } - - $kirby = App::instance(); - $root = $kirby->root('blueprints'); - $file = $root . '/' . $name . '.yml'; - - // first try to find a site blueprint, - // then check in the plugin extensions - if (F::exists($file, $root) !== true) { - $file = $kirby->extension('blueprints', $name); - } - - // now ensure that we always return the data array - if (is_string($file) === true && F::exists($file) === true) { - return static::$loaded[$name] = Data::read($file); - } elseif (is_array($file) === true) { - return static::$loaded[$name] = $file; - } elseif (is_callable($file) === true) { - return static::$loaded[$name] = $file($kirby); - } - - // neither a valid file nor array data - throw new NotFoundException([ - 'key' => 'blueprint.notFound', - 'data' => ['name' => $name] - ]); - } - - /** - * Used to translate any label, heading, etc. - * - * @param mixed $value - * @param mixed $fallback - * @return mixed - */ - protected function i18n($value, $fallback = null) - { - return I18n::translate($value, $fallback ?? $value); - } - - /** - * Checks if this is the default blueprint - * - * @return bool - */ - public function isDefault(): bool - { - return $this->name() === 'default'; - } - - /** - * Loads a blueprint from file or array - * - * @param string $name - * @return array - */ - public static function load(string $name): array - { - $props = static::find($name); - - $normalize = function ($props) use ($name) { - // inject the filename as name if no name is set - $props['name'] ??= $name; - - // normalize the title - $title = $props['title'] ?? ucfirst($props['name']); - - // translate the title - $props['title'] = I18n::translate($title, $title); - - return $props; - }; - - return $normalize($props); - } - - /** - * Returns the parent model - * - * @return \Kirby\Cms\Model - */ - public function model() - { - return $this->model; - } - - /** - * Returns the blueprint name - * - * @return string - */ - public function name(): string - { - return $this->props['name']; - } - - /** - * Normalizes all required props in a column setup - * - * @param string $tabName - * @param array $columns - * @return array - */ - protected function normalizeColumns(string $tabName, array $columns): array - { - foreach ($columns as $columnKey => $columnProps) { - // unset/remove column if its property is not array - if (is_array($columnProps) === false) { - unset($columns[$columnKey]); - continue; - } - - $columnProps = $this->convertFieldsToSections($tabName . '-col-' . $columnKey, $columnProps); - - // inject getting started info, if the sections are empty - if (empty($columnProps['sections']) === true) { - $columnProps['sections'] = [ - $tabName . '-info-' . $columnKey => [ - 'headline' => 'Column (' . ($columnProps['width'] ?? '1/1') . ')', - 'type' => 'info', - 'text' => 'No sections yet' - ] - ]; - } - - $columns[$columnKey] = array_merge($columnProps, [ - 'width' => $columnProps['width'] ?? '1/1', - 'sections' => $this->normalizeSections($tabName, $columnProps['sections'] ?? []) - ]); - } - - return $columns; - } - - /** - * @param array $items - * @return string - */ - public static function helpList(array $items): string - { - $md = []; - - foreach ($items as $item) { - $md[] = '- *' . $item . '*'; - } - - return PHP_EOL . implode(PHP_EOL, $md); - } - - /** - * Normalize field props for a single field - * - * @param array|string $props - * @return array - * @throws \Kirby\Exception\InvalidArgumentException If the filed name is missing or the field type is invalid - */ - public static function fieldProps($props): array - { - $props = static::extend($props); - - if (isset($props['name']) === false) { - throw new InvalidArgumentException('The field name is missing'); - } - - $name = $props['name']; - $type = $props['type'] ?? $name; - - if ($type !== 'group' && isset(Field::$types[$type]) === false) { - throw new InvalidArgumentException('Invalid field type ("' . $type . '")'); - } - - // support for nested fields - if (isset($props['fields']) === true) { - $props['fields'] = static::fieldsProps($props['fields']); - } - - // groups don't need all the crap - if ($type === 'group') { - return [ - 'fields' => $props['fields'], - 'name' => $name, - 'type' => $type, - ]; - } - - // add some useful defaults - return array_merge($props, [ - 'label' => $props['label'] ?? ucfirst($name), - 'name' => $name, - 'type' => $type, - 'width' => $props['width'] ?? '1/1', - ]); - } - - /** - * Creates an error field with the given error message - * - * @param string $name - * @param string $message - * @return array - */ - public static function fieldError(string $name, string $message): array - { - return [ - 'label' => 'Error', - 'name' => $name, - 'text' => strip_tags($message), - 'theme' => 'negative', - 'type' => 'info', - ]; - } - - /** - * Normalizes all fields and adds automatic labels, - * types and widths. - * - * @param array $fields - * @return array - */ - public static function fieldsProps($fields): array - { - if (is_array($fields) === false) { - $fields = []; - } - - foreach ($fields as $fieldName => $fieldProps) { - - // extend field from string - if (is_string($fieldProps) === true) { - $fieldProps = [ - 'extends' => $fieldProps, - 'name' => $fieldName - ]; - } - - // use the name as type definition - if ($fieldProps === true) { - $fieldProps = []; - } - - // unset / remove field if its property is false - if ($fieldProps === false) { - unset($fields[$fieldName]); - continue; - } - - // inject the name - $fieldProps['name'] = $fieldName; - - // create all props - try { - $fieldProps = static::fieldProps($fieldProps); - } catch (Throwable $e) { - $fieldProps = static::fieldError($fieldName, $e->getMessage()); - } - - // resolve field groups - if ($fieldProps['type'] === 'group') { - if (empty($fieldProps['fields']) === false && is_array($fieldProps['fields']) === true) { - $index = array_search($fieldName, array_keys($fields)); - $before = array_slice($fields, 0, $index); - $after = array_slice($fields, $index + 1); - $fields = array_merge($before, $fieldProps['fields'] ?? [], $after); - } else { - unset($fields[$fieldName]); - } - } else { - $fields[$fieldName] = $fieldProps; - } - } - - return $fields; - } - - /** - * Normalizes blueprint options. This must be used in the - * constructor of an extended class, if you want to make use of it. - * - * @param array|true|false|null|string $options - * @param array $defaults - * @param array $aliases - * @return array - */ - protected function normalizeOptions($options, array $defaults, array $aliases = []): array - { - // return defaults when options are not defined or set to true - if ($options === true) { - return $defaults; - } - - // set all options to false - if ($options === false) { - return array_map(fn () => false, $defaults); - } - - // extend options if possible - $options = $this->extend($options); - - foreach ($options as $key => $value) { - $alias = $aliases[$key] ?? null; - - if ($alias !== null) { - $options[$alias] ??= $value; - unset($options[$key]); - } - } - - return array_merge($defaults, $options); - } - - /** - * Normalizes all required keys in sections - * - * @param string $tabName - * @param array $sections - * @return array - */ - protected function normalizeSections(string $tabName, array $sections): array - { - foreach ($sections as $sectionName => $sectionProps) { - - // unset / remove section if its property is false - if ($sectionProps === false) { - unset($sections[$sectionName]); - continue; - } - - // fallback to default props when true is passed - if ($sectionProps === true) { - $sectionProps = []; - } - - // inject all section extensions - $sectionProps = $this->extend($sectionProps); - - $sections[$sectionName] = $sectionProps = array_merge($sectionProps, [ - 'name' => $sectionName, - 'type' => $type = $sectionProps['type'] ?? $sectionName - ]); - - if (empty($type) === true || is_string($type) === false) { - $sections[$sectionName] = [ - 'name' => $sectionName, - 'headline' => 'Invalid section type for section "' . $sectionName . '"', - 'type' => 'info', - 'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types)) - ]; - } elseif (isset(Section::$types[$type]) === false) { - $sections[$sectionName] = [ - 'name' => $sectionName, - 'headline' => 'Invalid section type ("' . $type . '")', - 'type' => 'info', - 'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types)) - ]; - } - - if ($sectionProps['type'] === 'fields') { - $fields = Blueprint::fieldsProps($sectionProps['fields'] ?? []); - - // inject guide fields guide - if (empty($fields) === true) { - $fields = [ - $tabName . '-info' => [ - 'label' => 'Fields', - 'text' => 'No fields yet', - 'type' => 'info' - ] - ]; - } else { - foreach ($fields as $fieldName => $fieldProps) { - if (isset($this->fields[$fieldName]) === true) { - $this->fields[$fieldName] = $fields[$fieldName] = [ - 'type' => 'info', - 'label' => $fieldProps['label'] ?? 'Error', - 'text' => 'The field name "' . $fieldName . '" already exists in your blueprint.', - 'theme' => 'negative' - ]; - } else { - $this->fields[$fieldName] = $fieldProps; - } - } - } - - $sections[$sectionName]['fields'] = $fields; - } - } - - // store all normalized sections - $this->sections = array_merge($this->sections, $sections); - - return $sections; - } - - /** - * Normalizes all required keys in tabs - * - * @param array $tabs - * @return array - */ - protected function normalizeTabs($tabs): array - { - if (is_array($tabs) === false) { - $tabs = []; - } - - foreach ($tabs as $tabName => $tabProps) { - - // unset / remove tab if its property is false - if ($tabProps === false) { - unset($tabs[$tabName]); - continue; - } - - // inject all tab extensions - $tabProps = $this->extend($tabProps); - - // inject a preset if available - $tabProps = $this->preset($tabProps); - - $tabProps = $this->convertFieldsToSections($tabName, $tabProps); - $tabProps = $this->convertSectionsToColumns($tabName, $tabProps); - - $tabs[$tabName] = array_merge($tabProps, [ - 'columns' => $this->normalizeColumns($tabName, $tabProps['columns'] ?? []), - 'icon' => $tabProps['icon'] ?? null, - 'label' => $this->i18n($tabProps['label'] ?? ucfirst($tabName)), - 'link' => $this->model->panel()->url(true) . '/?tab=' . $tabName, - 'name' => $tabName, - ]); - } - - return $this->tabs = $tabs; - } - - /** - * Injects a blueprint preset - * - * @param array $props - * @return array - */ - protected function preset(array $props): array - { - if (isset($props['preset']) === false) { - return $props; - } - - if (isset(static::$presets[$props['preset']]) === false) { - return $props; - } - - $preset = static::$presets[$props['preset']]; - - if (is_string($preset) === true) { - $preset = require $preset; - } - - return $preset($props); - } - - /** - * Returns a single section by name - * - * @param string $name - * @return \Kirby\Cms\Section|null - */ - public function section(string $name) - { - if (empty($this->sections[$name]) === true) { - return null; - } - - // get all props - $props = $this->sections[$name]; - - // inject the blueprint model - $props['model'] = $this->model(); - - // create a new section object - return new Section($props['type'], $props); - } - - /** - * Returns all sections - * - * @return array - */ - public function sections(): array - { - return A::map( - $this->sections, - fn ($section) => $this->section($section['name']) - ); - } - - /** - * Returns a single tab by name - * - * @param string|null $name - * @return array|null - */ - public function tab(?string $name = null): ?array - { - if ($name === null) { - return A::first($this->tabs); - } - - return $this->tabs[$name] ?? null; - } - - /** - * Returns all tabs - * - * @return array - */ - public function tabs(): array - { - return array_values($this->tabs); - } - - /** - * Returns the blueprint title - * - * @return string - */ - public function title(): string - { - return $this->props['title']; - } - - /** - * Converts the blueprint object to a plain array - * - * @return array - */ - public function toArray(): array - { - return $this->props; - } + public static $presets = []; + public static $loaded = []; + + protected $fields = []; + protected $model; + protected $props; + protected $sections = []; + protected $tabs = []; + + /** + * Magic getter/caller for any blueprint prop + * + * @param string $key + * @param array|null $arguments + * @return mixed + */ + public function __call(string $key, array $arguments = null) + { + return $this->props[$key] ?? null; + } + + /** + * Creates a new blueprint object with the given props + * + * @param array $props + * @throws \Kirby\Exception\InvalidArgumentException If the blueprint model is missing + */ + public function __construct(array $props) + { + if (empty($props['model']) === true) { + throw new InvalidArgumentException('A blueprint model is required'); + } + + if (is_a($props['model'], ModelWithContent::class) === false) { + throw new InvalidArgumentException('Invalid blueprint model'); + } + + $this->model = $props['model']; + + // the model should not be included in the props array + unset($props['model']); + + // extend the blueprint in general + $props = $this->extend($props); + + // apply any blueprint preset + $props = $this->preset($props); + + // normalize the name + $props['name'] ??= 'default'; + + // normalize and translate the title + $props['title'] = $this->i18n($props['title'] ?? ucfirst($props['name'])); + + // convert all shortcuts + $props = $this->convertFieldsToSections('main', $props); + $props = $this->convertSectionsToColumns('main', $props); + $props = $this->convertColumnsToTabs('main', $props); + + // normalize all tabs + $props['tabs'] = $this->normalizeTabs($props['tabs'] ?? []); + + $this->props = $props; + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->props ?? []; + } + + /** + * Converts all column definitions, that + * are not wrapped in a tab, into a generic tab + * + * @param string $tabName + * @param array $props + * @return array + */ + protected function convertColumnsToTabs(string $tabName, array $props): array + { + if (isset($props['columns']) === false) { + return $props; + } + + // wrap everything in a main tab + $props['tabs'] = [ + $tabName => [ + 'columns' => $props['columns'] + ] + ]; + + unset($props['columns']); + + return $props; + } + + /** + * Converts all field definitions, that are not + * wrapped in a fields section into a generic + * fields section. + * + * @param string $tabName + * @param array $props + * @return array + */ + protected function convertFieldsToSections(string $tabName, array $props): array + { + if (isset($props['fields']) === false) { + return $props; + } + + // wrap all fields in a section + $props['sections'] = [ + $tabName . '-fields' => [ + 'type' => 'fields', + 'fields' => $props['fields'] + ] + ]; + + unset($props['fields']); + + return $props; + } + + /** + * Converts all sections that are not wrapped in + * columns, into a single generic column. + * + * @param string $tabName + * @param array $props + * @return array + */ + protected function convertSectionsToColumns(string $tabName, array $props): array + { + if (isset($props['sections']) === false) { + return $props; + } + + // wrap everything in one big column + $props['columns'] = [ + [ + 'width' => '1/1', + 'sections' => $props['sections'] + ] + ]; + + unset($props['sections']); + + return $props; + } + + /** + * Extends the props with props from a given + * mixin, when an extends key is set or the + * props is just a string + * + * @param array|string $props + * @return array + */ + public static function extend($props): array + { + if (is_string($props) === true) { + $props = [ + 'extends' => $props + ]; + } + + $extends = $props['extends'] ?? null; + + if ($extends === null) { + return $props; + } + + foreach (A::wrap($extends) as $extend) { + try { + $mixin = static::find($extend); + $mixin = static::extend($mixin); + $props = A::merge($mixin, $props, A::MERGE_REPLACE); + } catch (Exception $e) { + // keep the props unextended if the snippet wasn't found + } + } + + // remove the extends flag + unset($props['extends']); + + return $props; + } + + /** + * Create a new blueprint for a model + * + * @param string $name + * @param string|null $fallback + * @param \Kirby\Cms\Model $model + * @return static|null + */ + public static function factory(string $name, string $fallback = null, Model $model) + { + try { + $props = static::load($name); + } catch (Exception $e) { + $props = $fallback !== null ? static::load($fallback) : null; + } + + if ($props === null) { + return null; + } + + // inject the parent model + $props['model'] = $model; + + return new static($props); + } + + /** + * Returns a single field definition by name + * + * @param string $name + * @return array|null + */ + public function field(string $name): ?array + { + return $this->fields[$name] ?? null; + } + + /** + * Returns all field definitions + * + * @return array + */ + public function fields(): array + { + return $this->fields; + } + + /** + * Find a blueprint by name + * + * @param string $name + * @return array + * @throws \Kirby\Exception\NotFoundException If the blueprint cannot be found + */ + public static function find(string $name): array + { + if (isset(static::$loaded[$name]) === true) { + return static::$loaded[$name]; + } + + $kirby = App::instance(); + $root = $kirby->root('blueprints'); + $file = $root . '/' . $name . '.yml'; + + // first try to find a site blueprint, + // then check in the plugin extensions + if (F::exists($file, $root) !== true) { + $file = $kirby->extension('blueprints', $name); + } + + // callback option can be return array or blueprint file path + if (is_callable($file) === true) { + $file = $file($kirby); + } + + // now ensure that we always return the data array + if (is_string($file) === true && F::exists($file) === true) { + return static::$loaded[$name] = Data::read($file); + } elseif (is_array($file) === true) { + return static::$loaded[$name] = $file; + } + + // neither a valid file nor array data + throw new NotFoundException([ + 'key' => 'blueprint.notFound', + 'data' => ['name' => $name] + ]); + } + + /** + * Used to translate any label, heading, etc. + * + * @param mixed $value + * @param mixed $fallback + * @return mixed + */ + protected function i18n($value, $fallback = null) + { + return I18n::translate($value, $fallback ?? $value); + } + + /** + * Checks if this is the default blueprint + * + * @return bool + */ + public function isDefault(): bool + { + return $this->name() === 'default'; + } + + /** + * Loads a blueprint from file or array + * + * @param string $name + * @return array + */ + public static function load(string $name): array + { + $props = static::find($name); + + $normalize = function ($props) use ($name) { + // inject the filename as name if no name is set + $props['name'] ??= $name; + + // normalize the title + $title = $props['title'] ?? ucfirst($props['name']); + + // translate the title + $props['title'] = I18n::translate($title, $title); + + return $props; + }; + + return $normalize($props); + } + + /** + * Returns the parent model + * + * @return \Kirby\Cms\Model + */ + public function model() + { + return $this->model; + } + + /** + * Returns the blueprint name + * + * @return string + */ + public function name(): string + { + return $this->props['name']; + } + + /** + * Normalizes all required props in a column setup + * + * @param string $tabName + * @param array $columns + * @return array + */ + protected function normalizeColumns(string $tabName, array $columns): array + { + foreach ($columns as $columnKey => $columnProps) { + // unset/remove column if its property is not array + if (is_array($columnProps) === false) { + unset($columns[$columnKey]); + continue; + } + + $columnProps = $this->convertFieldsToSections($tabName . '-col-' . $columnKey, $columnProps); + + // inject getting started info, if the sections are empty + if (empty($columnProps['sections']) === true) { + $columnProps['sections'] = [ + $tabName . '-info-' . $columnKey => [ + 'headline' => 'Column (' . ($columnProps['width'] ?? '1/1') . ')', + 'type' => 'info', + 'text' => 'No sections yet' + ] + ]; + } + + $columns[$columnKey] = array_merge($columnProps, [ + 'width' => $columnProps['width'] ?? '1/1', + 'sections' => $this->normalizeSections($tabName, $columnProps['sections'] ?? []) + ]); + } + + return $columns; + } + + /** + * @param array $items + * @return string + */ + public static function helpList(array $items): string + { + $md = []; + + foreach ($items as $item) { + $md[] = '- *' . $item . '*'; + } + + return PHP_EOL . implode(PHP_EOL, $md); + } + + /** + * Normalize field props for a single field + * + * @param array|string $props + * @return array + * @throws \Kirby\Exception\InvalidArgumentException If the filed name is missing or the field type is invalid + */ + public static function fieldProps($props): array + { + $props = static::extend($props); + + if (isset($props['name']) === false) { + throw new InvalidArgumentException('The field name is missing'); + } + + $name = $props['name']; + $type = $props['type'] ?? $name; + + if ($type !== 'group' && isset(Field::$types[$type]) === false) { + throw new InvalidArgumentException('Invalid field type ("' . $type . '")'); + } + + // support for nested fields + if (isset($props['fields']) === true) { + $props['fields'] = static::fieldsProps($props['fields']); + } + + // groups don't need all the crap + if ($type === 'group') { + return [ + 'fields' => $props['fields'], + 'name' => $name, + 'type' => $type, + ]; + } + + // add some useful defaults + return array_merge($props, [ + 'label' => $props['label'] ?? ucfirst($name), + 'name' => $name, + 'type' => $type, + 'width' => $props['width'] ?? '1/1', + ]); + } + + /** + * Creates an error field with the given error message + * + * @param string $name + * @param string $message + * @return array + */ + public static function fieldError(string $name, string $message): array + { + return [ + 'label' => 'Error', + 'name' => $name, + 'text' => strip_tags($message), + 'theme' => 'negative', + 'type' => 'info', + ]; + } + + /** + * Normalizes all fields and adds automatic labels, + * types and widths. + * + * @param array $fields + * @return array + */ + public static function fieldsProps($fields): array + { + if (is_array($fields) === false) { + $fields = []; + } + + foreach ($fields as $fieldName => $fieldProps) { + // extend field from string + if (is_string($fieldProps) === true) { + $fieldProps = [ + 'extends' => $fieldProps, + 'name' => $fieldName + ]; + } + + // use the name as type definition + if ($fieldProps === true) { + $fieldProps = []; + } + + // unset / remove field if its property is false + if ($fieldProps === false) { + unset($fields[$fieldName]); + continue; + } + + // inject the name + $fieldProps['name'] = $fieldName; + + // create all props + try { + $fieldProps = static::fieldProps($fieldProps); + } catch (Throwable $e) { + $fieldProps = static::fieldError($fieldName, $e->getMessage()); + } + + // resolve field groups + if ($fieldProps['type'] === 'group') { + if (empty($fieldProps['fields']) === false && is_array($fieldProps['fields']) === true) { + $index = array_search($fieldName, array_keys($fields)); + $before = array_slice($fields, 0, $index); + $after = array_slice($fields, $index + 1); + $fields = array_merge($before, $fieldProps['fields'] ?? [], $after); + } else { + unset($fields[$fieldName]); + } + } else { + $fields[$fieldName] = $fieldProps; + } + } + + return $fields; + } + + /** + * Normalizes blueprint options. This must be used in the + * constructor of an extended class, if you want to make use of it. + * + * @param array|true|false|null|string $options + * @param array $defaults + * @param array $aliases + * @return array + */ + protected function normalizeOptions($options, array $defaults, array $aliases = []): array + { + // return defaults when options are not defined or set to true + if ($options === true) { + return $defaults; + } + + // set all options to false + if ($options === false) { + return array_map(fn () => false, $defaults); + } + + // extend options if possible + $options = $this->extend($options); + + foreach ($options as $key => $value) { + $alias = $aliases[$key] ?? null; + + if ($alias !== null) { + $options[$alias] ??= $value; + unset($options[$key]); + } + } + + return array_merge($defaults, $options); + } + + /** + * Normalizes all required keys in sections + * + * @param string $tabName + * @param array $sections + * @return array + */ + protected function normalizeSections(string $tabName, array $sections): array + { + foreach ($sections as $sectionName => $sectionProps) { + // unset / remove section if its property is false + if ($sectionProps === false) { + unset($sections[$sectionName]); + continue; + } + + // fallback to default props when true is passed + if ($sectionProps === true) { + $sectionProps = []; + } + + // inject all section extensions + $sectionProps = $this->extend($sectionProps); + + $sections[$sectionName] = $sectionProps = array_merge($sectionProps, [ + 'name' => $sectionName, + 'type' => $type = $sectionProps['type'] ?? $sectionName + ]); + + if (empty($type) === true || is_string($type) === false) { + $sections[$sectionName] = [ + 'name' => $sectionName, + 'headline' => 'Invalid section type for section "' . $sectionName . '"', + 'type' => 'info', + 'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types)) + ]; + } elseif (isset(Section::$types[$type]) === false) { + $sections[$sectionName] = [ + 'name' => $sectionName, + 'headline' => 'Invalid section type ("' . $type . '")', + 'type' => 'info', + 'text' => 'The following section types are available: ' . $this->helpList(array_keys(Section::$types)) + ]; + } + + if ($sectionProps['type'] === 'fields') { + $fields = Blueprint::fieldsProps($sectionProps['fields'] ?? []); + + // inject guide fields guide + if (empty($fields) === true) { + $fields = [ + $tabName . '-info' => [ + 'label' => 'Fields', + 'text' => 'No fields yet', + 'type' => 'info' + ] + ]; + } else { + foreach ($fields as $fieldName => $fieldProps) { + if (isset($this->fields[$fieldName]) === true) { + $this->fields[$fieldName] = $fields[$fieldName] = [ + 'type' => 'info', + 'label' => $fieldProps['label'] ?? 'Error', + 'text' => 'The field name "' . $fieldName . '" already exists in your blueprint.', + 'theme' => 'negative' + ]; + } else { + $this->fields[$fieldName] = $fieldProps; + } + } + } + + $sections[$sectionName]['fields'] = $fields; + } + } + + // store all normalized sections + $this->sections = array_merge($this->sections, $sections); + + return $sections; + } + + /** + * Normalizes all required keys in tabs + * + * @param array $tabs + * @return array + */ + protected function normalizeTabs($tabs): array + { + if (is_array($tabs) === false) { + $tabs = []; + } + + foreach ($tabs as $tabName => $tabProps) { + // unset / remove tab if its property is false + if ($tabProps === false) { + unset($tabs[$tabName]); + continue; + } + + // inject all tab extensions + $tabProps = $this->extend($tabProps); + + // inject a preset if available + $tabProps = $this->preset($tabProps); + + $tabProps = $this->convertFieldsToSections($tabName, $tabProps); + $tabProps = $this->convertSectionsToColumns($tabName, $tabProps); + + $tabs[$tabName] = array_merge($tabProps, [ + 'columns' => $this->normalizeColumns($tabName, $tabProps['columns'] ?? []), + 'icon' => $tabProps['icon'] ?? null, + 'label' => $this->i18n($tabProps['label'] ?? ucfirst($tabName)), + 'link' => $this->model->panel()->url(true) . '/?tab=' . $tabName, + 'name' => $tabName, + ]); + } + + return $this->tabs = $tabs; + } + + /** + * Injects a blueprint preset + * + * @param array $props + * @return array + */ + protected function preset(array $props): array + { + if (isset($props['preset']) === false) { + return $props; + } + + if (isset(static::$presets[$props['preset']]) === false) { + return $props; + } + + $preset = static::$presets[$props['preset']]; + + if (is_string($preset) === true) { + $preset = require $preset; + } + + return $preset($props); + } + + /** + * Returns a single section by name + * + * @param string $name + * @return \Kirby\Cms\Section|null + */ + public function section(string $name) + { + if (empty($this->sections[$name]) === true) { + return null; + } + + // get all props + $props = $this->sections[$name]; + + // inject the blueprint model + $props['model'] = $this->model(); + + // create a new section object + return new Section($props['type'], $props); + } + + /** + * Returns all sections + * + * @return array + */ + public function sections(): array + { + return A::map( + $this->sections, + fn ($section) => $this->section($section['name']) + ); + } + + /** + * Returns a single tab by name + * + * @param string|null $name + * @return array|null + */ + public function tab(?string $name = null): ?array + { + if ($name === null) { + return A::first($this->tabs); + } + + return $this->tabs[$name] ?? null; + } + + /** + * Returns all tabs + * + * @return array + */ + public function tabs(): array + { + return array_values($this->tabs); + } + + /** + * Returns the blueprint title + * + * @return string + */ + public function title(): string + { + return $this->props['title']; + } + + /** + * Converts the blueprint object to a plain array + * + * @return array + */ + public function toArray(): array + { + return $this->props; + } } diff --git a/kirby/src/Cms/Collection.php b/kirby/src/Cms/Collection.php index 01f7b45..fb8f5f9 100644 --- a/kirby/src/Cms/Collection.php +++ b/kirby/src/Cms/Collection.php @@ -24,315 +24,316 @@ use Kirby\Toolkit\Str; */ class Collection extends BaseCollection { - use HasMethods; + use HasMethods; - /** - * Stores the parent object, which is needed - * in some collections to get the finder methods right. - * - * @var object - */ - protected $parent; + /** + * Stores the parent object, which is needed + * in some collections to get the finder methods right. + * + * @var object + */ + protected $parent; - /** - * Magic getter function - * - * @param string $key - * @param mixed $arguments - * @return mixed - */ - public function __call(string $key, $arguments) - { - // collection methods - if ($this->hasMethod($key) === true) { - return $this->callMethod($key, $arguments); - } - } + /** + * Magic getter function + * + * @param string $key + * @param mixed $arguments + * @return mixed + */ + public function __call(string $key, $arguments) + { + // collection methods + if ($this->hasMethod($key) === true) { + return $this->callMethod($key, $arguments); + } + } - /** - * Creates a new Collection with the given objects - * - * @param array $objects - * @param object|null $parent - */ - public function __construct($objects = [], $parent = null) - { - $this->parent = $parent; + /** + * Creates a new Collection with the given objects + * + * @param array $objects + * @param object|null $parent + */ + public function __construct($objects = [], $parent = null) + { + $this->parent = $parent; - foreach ($objects as $object) { - $this->add($object); - } - } + foreach ($objects as $object) { + $this->add($object); + } + } - /** - * Internal setter for each object in the Collection. - * This takes care of Component validation and of setting - * the collection prop on each object correctly. - * - * @param string $id - * @param object $object - */ - public function __set(string $id, $object) - { - $this->data[$id] = $object; - } + /** + * Internal setter for each object in the Collection. + * This takes care of Component validation and of setting + * the collection prop on each object correctly. + * + * @param string $id + * @param object $object + * @return void + */ + public function __set(string $id, $object): void + { + $this->data[$id] = $object; + } - /** - * Adds a single object or - * an entire second collection to the - * current collection - * - * @param mixed $object - */ - public function add($object) - { - if (is_a($object, self::class) === true) { - $this->data = array_merge($this->data, $object->data); - } elseif (is_object($object) === true && method_exists($object, 'id') === true) { - $this->__set($object->id(), $object); - } else { - $this->append($object); - } + /** + * Adds a single object or + * an entire second collection to the + * current collection + * + * @param mixed $object + */ + public function add($object) + { + if (is_a($object, self::class) === true) { + $this->data = array_merge($this->data, $object->data); + } elseif (is_object($object) === true && method_exists($object, 'id') === true) { + $this->__set($object->id(), $object); + } else { + $this->append($object); + } - return $this; - } + return $this; + } - /** - * Appends an element to the data array - * - * @param mixed ...$args - * @param mixed $key Optional collection key, will be determined from the item if not given - * @param mixed $item - * @return \Kirby\Cms\Collection - */ - public function append(...$args) - { - if (count($args) === 1) { - // try to determine the key from the provided item - if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) { - return parent::append($args[0]->id(), $args[0]); - } else { - return parent::append($args[0]); - } - } + /** + * Appends an element to the data array + * + * @param mixed ...$args + * @param mixed $key Optional collection key, will be determined from the item if not given + * @param mixed $item + * @return \Kirby\Cms\Collection + */ + public function append(...$args) + { + if (count($args) === 1) { + // try to determine the key from the provided item + if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) { + return parent::append($args[0]->id(), $args[0]); + } else { + return parent::append($args[0]); + } + } - return parent::append(...$args); - } + return parent::append(...$args); + } - /** - * Groups the items by a given field or callback. Returns a collection - * with an item for each group and a collection for each group. - * - * @param string|Closure $field - * @param bool $i Ignore upper/lowercase for group names - * @return \Kirby\Cms\Collection - * @throws \Kirby\Exception\Exception - */ - public function group($field, bool $i = true) - { - if (is_string($field) === true) { - $groups = new Collection([], $this->parent()); + /** + * Groups the items by a given field or callback. Returns a collection + * with an item for each group and a collection for each group. + * + * @param string|Closure $field + * @param bool $i Ignore upper/lowercase for group names + * @return \Kirby\Cms\Collection + * @throws \Kirby\Exception\Exception + */ + public function group($field, bool $i = true) + { + if (is_string($field) === true) { + $groups = new Collection([], $this->parent()); - foreach ($this->data as $key => $item) { - $value = $this->getAttribute($item, $field); + foreach ($this->data as $key => $item) { + $value = $this->getAttribute($item, $field); - // make sure that there's always a proper value to group by - if (!$value) { - throw new InvalidArgumentException('Invalid grouping value for key: ' . $key); - } + // make sure that there's always a proper value to group by + if (!$value) { + throw new InvalidArgumentException('Invalid grouping value for key: ' . $key); + } - // ignore upper/lowercase for group names - if ($i) { - $value = Str::lower($value); - } + // ignore upper/lowercase for group names + if ($i) { + $value = Str::lower($value); + } - if (isset($groups->data[$value]) === false) { - // create a new entry for the group if it does not exist yet - $groups->data[$value] = new static([$key => $item]); - } else { - // add the item to an existing group - $groups->data[$value]->set($key, $item); - } - } + if (isset($groups->data[$value]) === false) { + // create a new entry for the group if it does not exist yet + $groups->data[$value] = new static([$key => $item]); + } else { + // add the item to an existing group + $groups->data[$value]->set($key, $item); + } + } - return $groups; - } + return $groups; + } - return parent::group($field, $i); - } + return parent::group($field, $i); + } - /** - * Checks if the given object or id - * is in the collection - * - * @param string|object $key - * @return bool - */ - public function has($key): bool - { - if (is_object($key) === true) { - $key = $key->id(); - } + /** + * Checks if the given object or id + * is in the collection + * + * @param string|object $key + * @return bool + */ + public function has($key): bool + { + if (is_object($key) === true) { + $key = $key->id(); + } - return parent::has($key); - } + return parent::has($key); + } - /** - * Correct position detection for objects. - * The method will automatically detect objects - * or ids and then search accordingly. - * - * @param string|object $needle - * @return int - */ - public function indexOf($needle): int - { - if (is_string($needle) === true) { - return array_search($needle, $this->keys()); - } + /** + * Correct position detection for objects. + * The method will automatically detect objects + * or ids and then search accordingly. + * + * @param string|object $needle + * @return int + */ + public function indexOf($needle): int + { + if (is_string($needle) === true) { + return array_search($needle, $this->keys()); + } - return array_search($needle->id(), $this->keys()); - } + return array_search($needle->id(), $this->keys()); + } - /** - * Returns a Collection without the given element(s) - * - * @param mixed ...$keys any number of keys, passed as individual arguments - * @return \Kirby\Cms\Collection - */ - public function not(...$keys) - { - $collection = $this->clone(); + /** + * Returns a Collection without the given element(s) + * + * @param mixed ...$keys any number of keys, passed as individual arguments + * @return \Kirby\Cms\Collection + */ + public function not(...$keys) + { + $collection = $this->clone(); - foreach ($keys as $key) { - if (is_array($key) === true) { - return $this->not(...$key); - } elseif (is_a($key, 'Kirby\Toolkit\Collection') === true) { - $collection = $collection->not(...$key->keys()); - } elseif (is_object($key) === true) { - $key = $key->id(); - } + foreach ($keys as $key) { + if (is_array($key) === true) { + return $this->not(...$key); + } elseif (is_a($key, 'Kirby\Toolkit\Collection') === true) { + $collection = $collection->not(...$key->keys()); + } elseif (is_object($key) === true) { + $key = $key->id(); + } - unset($collection->{$key}); - } + unset($collection->{$key}); + } - return $collection; - } + return $collection; + } - /** - * Add pagination and return a sliced set of data. - * - * @param mixed ...$arguments - * @return \Kirby\Cms\Collection - */ - public function paginate(...$arguments) - { - $this->pagination = Pagination::for($this, ...$arguments); + /** + * Add pagination and return a sliced set of data. + * + * @param mixed ...$arguments + * @return \Kirby\Cms\Collection + */ + public function paginate(...$arguments) + { + $this->pagination = Pagination::for($this, ...$arguments); - // slice and clone the collection according to the pagination - return $this->slice($this->pagination->offset(), $this->pagination->limit()); - } + // slice and clone the collection according to the pagination + return $this->slice($this->pagination->offset(), $this->pagination->limit()); + } - /** - * Returns the parent model - * - * @return \Kirby\Cms\Model - */ - public function parent() - { - return $this->parent; - } + /** + * Returns the parent model + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } - /** - * Prepends an element to the data array - * - * @param mixed ...$args - * @param mixed $key Optional collection key, will be determined from the item if not given - * @param mixed $item - * @return \Kirby\Cms\Collection - */ - public function prepend(...$args) - { - if (count($args) === 1) { - // try to determine the key from the provided item - if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) { - return parent::prepend($args[0]->id(), $args[0]); - } else { - return parent::prepend($args[0]); - } - } + /** + * Prepends an element to the data array + * + * @param mixed ...$args + * @param mixed $key Optional collection key, will be determined from the item if not given + * @param mixed $item + * @return \Kirby\Cms\Collection + */ + public function prepend(...$args) + { + if (count($args) === 1) { + // try to determine the key from the provided item + if (is_object($args[0]) === true && is_callable([$args[0], 'id']) === true) { + return parent::prepend($args[0]->id(), $args[0]); + } else { + return parent::prepend($args[0]); + } + } - return parent::prepend(...$args); - } + return parent::prepend(...$args); + } - /** - * Runs a combination of filter, sort, not, - * offset, limit, search and paginate on the collection. - * Any part of the query is optional. - * - * @param array $arguments - * @return static - */ - public function query(array $arguments = []) - { - $paginate = $arguments['paginate'] ?? null; - $search = $arguments['search'] ?? null; + /** + * Runs a combination of filter, sort, not, + * offset, limit, search and paginate on the collection. + * Any part of the query is optional. + * + * @param array $arguments + * @return static + */ + public function query(array $arguments = []) + { + $paginate = $arguments['paginate'] ?? null; + $search = $arguments['search'] ?? null; - unset($arguments['paginate']); + unset($arguments['paginate']); - $result = parent::query($arguments); + $result = parent::query($arguments); - if (empty($search) === false) { - if (is_array($search) === true) { - $result = $result->search($search['query'] ?? null, $search['options'] ?? []); - } else { - $result = $result->search($search); - } - } + if (empty($search) === false) { + if (is_array($search) === true) { + $result = $result->search($search['query'] ?? null, $search['options'] ?? []); + } else { + $result = $result->search($search); + } + } - if (empty($paginate) === false) { - $result = $result->paginate($paginate); - } + if (empty($paginate) === false) { + $result = $result->paginate($paginate); + } - return $result; - } + return $result; + } - /** - * Removes an object - * - * @param mixed $key the name of the key - */ - public function remove($key) - { - if (is_object($key) === true) { - $key = $key->id(); - } + /** + * Removes an object + * + * @param mixed $key the name of the key + */ + public function remove($key) + { + if (is_object($key) === true) { + $key = $key->id(); + } - return parent::remove($key); - } + return parent::remove($key); + } - /** - * Searches the collection - * - * @param string|null $query - * @param array $params - * @return self - */ - public function search(string $query = null, $params = []) - { - return Search::collection($this, $query, $params); - } + /** + * Searches the collection + * + * @param string|null $query + * @param array $params + * @return self + */ + public function search(string $query = null, $params = []) + { + return Search::collection($this, $query, $params); + } - /** - * Converts all objects in the collection - * to an array. This can also take a callback - * function to further modify the array result. - * - * @param \Closure|null $map - * @return array - */ - public function toArray(Closure $map = null): array - { - return parent::toArray($map ?? fn ($object) => $object->toArray()); - } + /** + * Converts all objects in the collection + * to an array. This can also take a callback + * function to further modify the array result. + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + return parent::toArray($map ?? fn ($object) => $object->toArray()); + } } diff --git a/kirby/src/Cms/Collections.php b/kirby/src/Cms/Collections.php index b7f7d64..542ed03 100644 --- a/kirby/src/Cms/Collections.php +++ b/kirby/src/Cms/Collections.php @@ -22,120 +22,120 @@ use Kirby\Toolkit\Controller; */ class Collections { - /** - * Each collection is cached once it - * has been called, to avoid further - * processing on sequential calls to - * the same collection. - * - * @var array - */ - protected $cache = []; + /** + * Each collection is cached once it + * has been called, to avoid further + * processing on sequential calls to + * the same collection. + * + * @var array + */ + protected $cache = []; - /** - * Store of all collections - * - * @var array - */ - protected $collections = []; + /** + * Store of all collections + * + * @var array + */ + protected $collections = []; - /** - * Magic caller to enable something like - * `$collections->myCollection()` - * - * @param string $name - * @param array $arguments - * @return \Kirby\Cms\Collection|null - */ - public function __call(string $name, array $arguments = []) - { - return $this->get($name, ...$arguments); - } + /** + * Magic caller to enable something like + * `$collections->myCollection()` + * + * @param string $name + * @param array $arguments + * @return \Kirby\Cms\Collection|null + */ + public function __call(string $name, array $arguments = []) + { + return $this->get($name, ...$arguments); + } - /** - * Loads a collection by name if registered - * - * @param string $name - * @param array $data - * @return \Kirby\Cms\Collection|null - */ - public function get(string $name, array $data = []) - { - // if not yet loaded - if (isset($this->collections[$name]) === false) { - $this->collections[$name] = $this->load($name); - } + /** + * Loads a collection by name if registered + * + * @param string $name + * @param array $data + * @return \Kirby\Cms\Collection|null + */ + public function get(string $name, array $data = []) + { + // if not yet loaded + if (isset($this->collections[$name]) === false) { + $this->collections[$name] = $this->load($name); + } - // if not yet cached - if ( - isset($this->cache[$name]) === false || - $this->cache[$name]['data'] !== $data - ) { - $controller = new Controller($this->collections[$name]); - $this->cache[$name] = [ - 'result' => $controller->call(null, $data), - 'data' => $data - ]; - } + // if not yet cached + if ( + isset($this->cache[$name]) === false || + $this->cache[$name]['data'] !== $data + ) { + $controller = new Controller($this->collections[$name]); + $this->cache[$name] = [ + 'result' => $controller->call(null, $data), + 'data' => $data + ]; + } - // return cloned object - if (is_object($this->cache[$name]['result']) === true) { - return clone $this->cache[$name]['result']; - } + // return cloned object + if (is_object($this->cache[$name]['result']) === true) { + return clone $this->cache[$name]['result']; + } - return $this->cache[$name]['result']; - } + return $this->cache[$name]['result']; + } - /** - * Checks if a collection exists - * - * @param string $name - * @return bool - */ - public function has(string $name): bool - { - if (isset($this->collections[$name]) === true) { - return true; - } + /** + * Checks if a collection exists + * + * @param string $name + * @return bool + */ + public function has(string $name): bool + { + if (isset($this->collections[$name]) === true) { + return true; + } - try { - $this->load($name); - return true; - } catch (NotFoundException $e) { - return false; - } - } + try { + $this->load($name); + return true; + } catch (NotFoundException $e) { + return false; + } + } - /** - * Loads collection from php file in a - * given directory or from plugin extension. - * - * @param string $name - * @return mixed - * @throws \Kirby\Exception\NotFoundException - */ - public function load(string $name) - { - $kirby = App::instance(); + /** + * Loads collection from php file in a + * given directory or from plugin extension. + * + * @param string $name + * @return mixed + * @throws \Kirby\Exception\NotFoundException + */ + public function load(string $name) + { + $kirby = App::instance(); - // first check for collection file - $file = $kirby->root('collections') . '/' . $name . '.php'; + // first check for collection file + $file = $kirby->root('collections') . '/' . $name . '.php'; - if (is_file($file) === true) { - $collection = F::load($file); + if (is_file($file) === true) { + $collection = F::load($file); - if (is_a($collection, 'Closure')) { - return $collection; - } - } + if (is_a($collection, 'Closure')) { + return $collection; + } + } - // fallback to collections from plugins - $collections = $kirby->extensions('collections'); + // fallback to collections from plugins + $collections = $kirby->extensions('collections'); - if (isset($collections[$name]) === true) { - return $collections[$name]; - } + if (isset($collections[$name]) === true) { + return $collections[$name]; + } - throw new NotFoundException('The collection cannot be found'); - } + throw new NotFoundException('The collection cannot be found'); + } } diff --git a/kirby/src/Cms/Content.php b/kirby/src/Cms/Content.php index 66d5006..d0a6e11 100644 --- a/kirby/src/Cms/Content.php +++ b/kirby/src/Cms/Content.php @@ -16,253 +16,254 @@ use Kirby\Form\Form; */ class Content { - /** - * The raw data array - * - * @var array - */ - protected $data = []; + /** + * The raw data array + * + * @var array + */ + protected $data = []; - /** - * Cached field objects - * Once a field is being fetched - * it is added to this array for - * later reuse - * - * @var array - */ - protected $fields = []; + /** + * Cached field objects + * Once a field is being fetched + * it is added to this array for + * later reuse + * + * @var array + */ + protected $fields = []; - /** - * A potential parent object. - * Not necessarily needed. Especially - * for testing, but field methods might - * need it. - * - * @var Model - */ - protected $parent; + /** + * A potential parent object. + * Not necessarily needed. Especially + * for testing, but field methods might + * need it. + * + * @var Model + */ + protected $parent; - /** - * Magic getter for content fields - * - * @param string $name - * @param array $arguments - * @return \Kirby\Cms\Field - */ - public function __call(string $name, array $arguments = []) - { - return $this->get($name); - } + /** + * Magic getter for content fields + * + * @param string $name + * @param array $arguments + * @return \Kirby\Cms\Field + */ + public function __call(string $name, array $arguments = []) + { + return $this->get($name); + } - /** - * Creates a new Content object - * - * @param array|null $data - * @param object|null $parent - */ - public function __construct(array $data = [], $parent = null) - { - $this->data = $data; - $this->parent = $parent; - } + /** + * Creates a new Content object + * + * @param array|null $data + * @param object|null $parent + * @param bool $normalize Set to `false` if the input field keys are already lowercase + */ + public function __construct(array $data = [], $parent = null, bool $normalize = true) + { + if ($normalize === true) { + $data = array_change_key_case($data, CASE_LOWER); + } - /** - * Same as `self::data()` to improve - * `var_dump` output - * - * @see self::data() - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + $this->data = $data; + $this->parent = $parent; + } - /** - * Converts the content to a new blueprint - * - * @param string $to - * @return array - */ - public function convertTo(string $to): array - { - // prepare data - $data = []; - $content = $this; + /** + * Same as `self::data()` to improve + * `var_dump` output + * + * @see self::data() + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - // blueprints - $old = $this->parent->blueprint(); - $subfolder = dirname($old->name()); - $new = Blueprint::factory($subfolder . '/' . $to, $subfolder . '/default', $this->parent); + /** + * Converts the content to a new blueprint + * + * @param string $to + * @return array + */ + public function convertTo(string $to): array + { + // prepare data + $data = []; + $content = $this; - // forms - $oldForm = new Form(['fields' => $old->fields(), 'model' => $this->parent]); - $newForm = new Form(['fields' => $new->fields(), 'model' => $this->parent]); + // blueprints + $old = $this->parent->blueprint(); + $subfolder = dirname($old->name()); + $new = Blueprint::factory($subfolder . '/' . $to, $subfolder . '/default', $this->parent); - // fields - $oldFields = $oldForm->fields(); - $newFields = $newForm->fields(); + // forms + $oldForm = new Form(['fields' => $old->fields(), 'model' => $this->parent]); + $newForm = new Form(['fields' => $new->fields(), 'model' => $this->parent]); - // go through all fields of new template - foreach ($newFields as $newField) { - $name = $newField->name(); - $oldField = $oldFields->get($name); + // fields + $oldFields = $oldForm->fields(); + $newFields = $newForm->fields(); - // field name and type matches with old template - if ($oldField && $oldField->type() === $newField->type()) { - $data[$name] = $content->get($name)->value(); - } else { - $data[$name] = $newField->default(); - } - } + // go through all fields of new template + foreach ($newFields as $newField) { + $name = $newField->name(); + $oldField = $oldFields->get($name); - // preserve existing fields - return array_merge($this->data, $data); - } + // field name and type matches with old template + if ($oldField && $oldField->type() === $newField->type()) { + $data[$name] = $content->get($name)->value(); + } else { + $data[$name] = $newField->default(); + } + } - /** - * Returns the raw data array - * - * @return array - */ - public function data(): array - { - return $this->data; - } + // preserve existing fields + return array_merge($this->data, $data); + } - /** - * Returns all registered field objects - * - * @return array - */ - public function fields(): array - { - foreach ($this->data as $key => $value) { - $this->get($key); - } - return $this->fields; - } + /** + * Returns the raw data array + * + * @return array + */ + public function data(): array + { + return $this->data; + } - /** - * Returns either a single field object - * or all registered fields - * - * @param string|null $key - * @return \Kirby\Cms\Field|array - */ - public function get(string $key = null) - { - if ($key === null) { - return $this->fields(); - } + /** + * Returns all registered field objects + * + * @return array + */ + public function fields(): array + { + foreach ($this->data as $key => $value) { + $this->get($key); + } + return $this->fields; + } - $key = strtolower($key); + /** + * Returns either a single field object + * or all registered fields + * + * @param string|null $key + * @return \Kirby\Cms\Field|array + */ + public function get(string $key = null) + { + if ($key === null) { + return $this->fields(); + } - if (isset($this->fields[$key])) { - return $this->fields[$key]; - } + $key = strtolower($key); - // fetch the value no matter the case - $data = $this->data(); - $value = $data[$key] ?? array_change_key_case($data)[$key] ?? null; + if (isset($this->fields[$key])) { + return $this->fields[$key]; + } - return $this->fields[$key] = new Field($this->parent, $key, $value); - } + $value = $this->data()[$key] ?? null; - /** - * Checks if a content field is set - * - * @param string $key - * @return bool - */ - public function has(string $key): bool - { - $key = strtolower($key); - $data = array_change_key_case($this->data); + return $this->fields[$key] = new Field($this->parent, $key, $value); + } - return isset($data[$key]) === true; - } + /** + * Checks if a content field is set + * + * @param string $key + * @return bool + */ + public function has(string $key): bool + { + return isset($this->data[strtolower($key)]) === true; + } - /** - * Returns all field keys - * - * @return array - */ - public function keys(): array - { - return array_keys($this->data()); - } + /** + * Returns all field keys + * + * @return array + */ + public function keys(): array + { + return array_keys($this->data()); + } - /** - * Returns a clone of the content object - * without the fields, specified by the - * passed key(s) - * - * @param string ...$keys - * @return static - */ - public function not(...$keys) - { - $copy = clone $this; - $copy->fields = null; + /** + * Returns a clone of the content object + * without the fields, specified by the + * passed key(s) + * + * @param string ...$keys + * @return static + */ + public function not(...$keys) + { + $copy = clone $this; + $copy->fields = null; - foreach ($keys as $key) { - unset($copy->data[$key]); - } + foreach ($keys as $key) { + unset($copy->data[strtolower($key)]); + } - return $copy; - } + return $copy; + } - /** - * Returns the parent - * Site, Page, File or User object - * - * @return \Kirby\Cms\Model - */ - public function parent() - { - return $this->parent; - } + /** + * Returns the parent + * Site, Page, File or User object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } - /** - * Set the parent model - * - * @param \Kirby\Cms\Model $parent - * @return $this - */ - public function setParent(Model $parent) - { - $this->parent = $parent; - return $this; - } + /** + * Set the parent model + * + * @param \Kirby\Cms\Model $parent + * @return $this + */ + public function setParent(Model $parent) + { + $this->parent = $parent; + return $this; + } - /** - * Returns the raw data array - * - * @see self::data() - * @return array - */ - public function toArray(): array - { - return $this->data(); - } + /** + * Returns the raw data array + * + * @see self::data() + * @return array + */ + public function toArray(): array + { + return $this->data(); + } - /** - * Updates the content and returns - * a cloned object - * - * @param array|null $content - * @param bool $overwrite - * @return $this - */ - public function update(array $content = null, bool $overwrite = false) - { - $this->data = $overwrite === true ? (array)$content : array_merge($this->data, (array)$content); + /** + * Updates the content and returns + * a cloned object + * + * @param array|null $content + * @param bool $overwrite + * @return $this + */ + public function update(array $content = null, bool $overwrite = false) + { + $content = array_change_key_case((array)$content, CASE_LOWER); + $this->data = $overwrite === true ? $content : array_merge($this->data, $content); - // clear cache of Field objects - $this->fields = []; + // clear cache of Field objects + $this->fields = []; - return $this; - } + return $this; + } } diff --git a/kirby/src/Cms/ContentLock.php b/kirby/src/Cms/ContentLock.php index c3daf2f..f938116 100644 --- a/kirby/src/Cms/ContentLock.php +++ b/kirby/src/Cms/ContentLock.php @@ -17,216 +17,216 @@ use Kirby\Exception\PermissionException; */ class ContentLock { - /** - * Lock data - * - * @var array - */ - protected $data; + /** + * Lock data + * + * @var array + */ + protected $data; - /** - * The model to manage locking/unlocking for - * - * @var ModelWithContent - */ - protected $model; + /** + * The model to manage locking/unlocking for + * + * @var ModelWithContent + */ + protected $model; - /** - * @param \Kirby\Cms\ModelWithContent $model - */ - public function __construct(ModelWithContent $model) - { - $this->model = $model; - $this->data = $this->kirby()->locks()->get($model); - } + /** + * @param \Kirby\Cms\ModelWithContent $model + */ + public function __construct(ModelWithContent $model) + { + $this->model = $model; + $this->data = $this->kirby()->locks()->get($model); + } - /** - * Clears the lock unconditionally - * - * @return bool - */ - protected function clearLock(): bool - { - // if no lock exists, skip - if (isset($this->data['lock']) === false) { - return true; - } + /** + * Clears the lock unconditionally + * + * @return bool + */ + protected function clearLock(): bool + { + // if no lock exists, skip + if (isset($this->data['lock']) === false) { + return true; + } - // remove lock - unset($this->data['lock']); + // remove lock + unset($this->data['lock']); - return $this->kirby()->locks()->set($this->model, $this->data); - } + return $this->kirby()->locks()->set($this->model, $this->data); + } - /** - * Sets lock with the current user - * - * @return bool - * @throws \Kirby\Exception\DuplicateException - */ - public function create(): bool - { - // check if model is already locked by another user - if ( - isset($this->data['lock']) === true && - $this->data['lock']['user'] !== $this->user()->id() - ) { - $id = ContentLocks::id($this->model); - throw new DuplicateException($id . ' is already locked'); - } + /** + * Sets lock with the current user + * + * @return bool + * @throws \Kirby\Exception\DuplicateException + */ + public function create(): bool + { + // check if model is already locked by another user + if ( + isset($this->data['lock']) === true && + $this->data['lock']['user'] !== $this->user()->id() + ) { + $id = ContentLocks::id($this->model); + throw new DuplicateException($id . ' is already locked'); + } - $this->data['lock'] = [ - 'user' => $this->user()->id(), - 'time' => time() - ]; + $this->data['lock'] = [ + 'user' => $this->user()->id(), + 'time' => time() + ]; - return $this->kirby()->locks()->set($this->model, $this->data); - } + return $this->kirby()->locks()->set($this->model, $this->data); + } - /** - * Returns either `false` or array with `user`, `email`, - * `time` and `unlockable` keys - * - * @return array|bool - */ - public function get() - { - $data = $this->data['lock'] ?? []; + /** + * Returns either `false` or array with `user`, `email`, + * `time` and `unlockable` keys + * + * @return array|bool + */ + public function get() + { + $data = $this->data['lock'] ?? []; - if (empty($data) === false && $data['user'] !== $this->user()->id()) { - if ($user = $this->kirby()->user($data['user'])) { - $time = (int)($data['time']); + if (empty($data) === false && $data['user'] !== $this->user()->id()) { + if ($user = $this->kirby()->user($data['user'])) { + $time = (int)($data['time']); - return [ - 'user' => $user->id(), - 'email' => $user->email(), - 'time' => $time, - 'unlockable' => ($time + 60) <= time() - ]; - } + return [ + 'user' => $user->id(), + 'email' => $user->email(), + 'time' => $time, + 'unlockable' => ($time + 60) <= time() + ]; + } - // clear lock if user not found - $this->clearLock(); - } + // clear lock if user not found + $this->clearLock(); + } - return false; - } + return false; + } - /** - * Returns if the model is locked by another user - * - * @return bool - */ - public function isLocked(): bool - { - $lock = $this->get(); + /** + * Returns if the model is locked by another user + * + * @return bool + */ + public function isLocked(): bool + { + $lock = $this->get(); - if ($lock !== false && $lock['user'] !== $this->user()->id()) { - return true; - } + if ($lock !== false && $lock['user'] !== $this->user()->id()) { + return true; + } - return false; - } + return false; + } - /** - * Returns if the current user's lock has been removed by another user - * - * @return bool - */ - public function isUnlocked(): bool - { - $data = $this->data['unlock'] ?? []; + /** + * Returns if the current user's lock has been removed by another user + * + * @return bool + */ + public function isUnlocked(): bool + { + $data = $this->data['unlock'] ?? []; - return in_array($this->user()->id(), $data) === true; - } + return in_array($this->user()->id(), $data) === true; + } - /** - * Returns the app instance - * - * @return \Kirby\Cms\App - */ - protected function kirby(): App - { - return $this->model->kirby(); - } + /** + * Returns the app instance + * + * @return \Kirby\Cms\App + */ + protected function kirby(): App + { + return $this->model->kirby(); + } - /** - * Removes lock of current user - * - * @return bool - * @throws \Kirby\Exception\LogicException - */ - public function remove(): bool - { - // if no lock exists, skip - if (isset($this->data['lock']) === false) { - return true; - } + /** + * Removes lock of current user + * + * @return bool + * @throws \Kirby\Exception\LogicException + */ + public function remove(): bool + { + // if no lock exists, skip + if (isset($this->data['lock']) === false) { + return true; + } - // check if lock was set by another user - if ($this->data['lock']['user'] !== $this->user()->id()) { - throw new LogicException([ - 'fallback' => 'The content lock can only be removed by the user who created it. Use unlock instead.', - 'httpCode' => 409 - ]); - } + // check if lock was set by another user + if ($this->data['lock']['user'] !== $this->user()->id()) { + throw new LogicException([ + 'fallback' => 'The content lock can only be removed by the user who created it. Use unlock instead.', + 'httpCode' => 409 + ]); + } - return $this->clearLock(); - } + return $this->clearLock(); + } - /** - * Removes unlock information for current user - * - * @return bool - */ - public function resolve(): bool - { - // if no unlocks exist, skip - if (isset($this->data['unlock']) === false) { - return true; - } + /** + * Removes unlock information for current user + * + * @return bool + */ + public function resolve(): bool + { + // if no unlocks exist, skip + if (isset($this->data['unlock']) === false) { + return true; + } - // remove user from unlock array - $this->data['unlock'] = array_diff( - $this->data['unlock'], - [$this->user()->id()] - ); + // remove user from unlock array + $this->data['unlock'] = array_diff( + $this->data['unlock'], + [$this->user()->id()] + ); - return $this->kirby()->locks()->set($this->model, $this->data); - } + return $this->kirby()->locks()->set($this->model, $this->data); + } - /** - * Removes current lock and adds lock user to unlock data - * - * @return bool - */ - public function unlock(): bool - { - // if no lock exists, skip - if (isset($this->data['lock']) === false) { - return true; - } + /** + * Removes current lock and adds lock user to unlock data + * + * @return bool + */ + public function unlock(): bool + { + // if no lock exists, skip + if (isset($this->data['lock']) === false) { + return true; + } - // add lock user to unlocked data - $this->data['unlock'] ??= []; - $this->data['unlock'][] = $this->data['lock']['user']; + // add lock user to unlocked data + $this->data['unlock'] ??= []; + $this->data['unlock'][] = $this->data['lock']['user']; - return $this->clearLock(); - } + return $this->clearLock(); + } - /** - * Returns currently authenticated user; - * throws exception if none is authenticated - * - * @return \Kirby\Cms\User - * @throws \Kirby\Exception\PermissionException - */ - protected function user(): User - { - if ($user = $this->kirby()->user()) { - return $user; - } + /** + * Returns currently authenticated user; + * throws exception if none is authenticated + * + * @return \Kirby\Cms\User + * @throws \Kirby\Exception\PermissionException + */ + protected function user(): User + { + if ($user = $this->kirby()->user()) { + return $user; + } - throw new PermissionException('No user authenticated.'); - } + throw new PermissionException('No user authenticated.'); + } } diff --git a/kirby/src/Cms/ContentLocks.php b/kirby/src/Cms/ContentLocks.php index f56d216..2d08d9f 100644 --- a/kirby/src/Cms/ContentLocks.php +++ b/kirby/src/Cms/ContentLocks.php @@ -18,211 +18,211 @@ use Kirby\Filesystem\F; */ class ContentLocks { - /** - * Data from the `.lock` files - * that have been read so far - * cached by `.lock` file path - * - * @var array - */ - protected $data = []; + /** + * Data from the `.lock` files + * that have been read so far + * cached by `.lock` file path + * + * @var array + */ + protected $data = []; - /** - * PHP file handles for all currently - * open `.lock` files - * - * @var array - */ - protected $handles = []; + /** + * PHP file handles for all currently + * open `.lock` files + * + * @var array + */ + protected $handles = []; - /** - * Closes the open file handles - * - * @codeCoverageIgnore - */ - public function __destruct() - { - foreach ($this->handles as $file => $handle) { - $this->closeHandle($file); - } - } + /** + * Closes the open file handles + * + * @codeCoverageIgnore + */ + public function __destruct() + { + foreach ($this->handles as $file => $handle) { + $this->closeHandle($file); + } + } - /** - * Removes the file lock and closes the file handle - * - * @param string $file - * @return void - * @throws \Kirby\Exception\Exception - */ - protected function closeHandle(string $file) - { - if (isset($this->handles[$file]) === false) { - return; - } + /** + * Removes the file lock and closes the file handle + * + * @param string $file + * @return void + * @throws \Kirby\Exception\Exception + */ + protected function closeHandle(string $file) + { + if (isset($this->handles[$file]) === false) { + return; + } - $handle = $this->handles[$file]; - $result = flock($handle, LOCK_UN) && fclose($handle); + $handle = $this->handles[$file]; + $result = flock($handle, LOCK_UN) && fclose($handle); - if ($result !== true) { - throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore - } + if ($result !== true) { + throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore + } - unset($this->handles[$file]); - } + unset($this->handles[$file]); + } - /** - * Returns the path to a model's lock file - * - * @param \Kirby\Cms\ModelWithContent $model - * @return string - */ - public static function file(ModelWithContent $model): string - { - return $model->contentFileDirectory() . '/.lock'; - } + /** + * Returns the path to a model's lock file + * + * @param \Kirby\Cms\ModelWithContent $model + * @return string + */ + public static function file(ModelWithContent $model): string + { + return $model->contentFileDirectory() . '/.lock'; + } - /** - * Returns the lock/unlock data for the specified model - * - * @param \Kirby\Cms\ModelWithContent $model - * @return array - */ - public function get(ModelWithContent $model): array - { - $file = static::file($model); - $id = static::id($model); + /** + * Returns the lock/unlock data for the specified model + * + * @param \Kirby\Cms\ModelWithContent $model + * @return array + */ + public function get(ModelWithContent $model): array + { + $file = static::file($model); + $id = static::id($model); - // return from cache if file was already loaded - if (isset($this->data[$file]) === true) { - return $this->data[$file][$id] ?? []; - } + // return from cache if file was already loaded + if (isset($this->data[$file]) === true) { + return $this->data[$file][$id] ?? []; + } - // first get a handle to ensure a file system lock - $handle = $this->handle($file); + // first get a handle to ensure a file system lock + $handle = $this->handle($file); - if (is_resource($handle) === true) { - // read data from file - clearstatcache(); - $filesize = filesize($file); + if (is_resource($handle) === true) { + // read data from file + clearstatcache(); + $filesize = filesize($file); - if ($filesize > 0) { - // always read the whole file - rewind($handle); - $string = fread($handle, $filesize); - $data = Data::decode($string, 'yaml'); - } - } + if ($filesize > 0) { + // always read the whole file + rewind($handle); + $string = fread($handle, $filesize); + $data = Data::decode($string, 'yaml'); + } + } - $this->data[$file] = $data ?? []; + $this->data[$file] = $data ?? []; - return $this->data[$file][$id] ?? []; - } + return $this->data[$file][$id] ?? []; + } - /** - * Returns the file handle to a `.lock` file - * - * @param string $file - * @param bool $create Whether to create the file if it does not exist - * @return resource|null File handle - * @throws \Kirby\Exception\Exception - */ - protected function handle(string $file, bool $create = false) - { - // check for an already open handle - if (isset($this->handles[$file]) === true) { - return $this->handles[$file]; - } + /** + * Returns the file handle to a `.lock` file + * + * @param string $file + * @param bool $create Whether to create the file if it does not exist + * @return resource|null File handle + * @throws \Kirby\Exception\Exception + */ + protected function handle(string $file, bool $create = false) + { + // check for an already open handle + if (isset($this->handles[$file]) === true) { + return $this->handles[$file]; + } - // don't create a file if not requested - if (is_file($file) !== true && $create !== true) { - return null; - } + // don't create a file if not requested + if (is_file($file) !== true && $create !== true) { + return null; + } - $handle = @fopen($file, 'c+b'); - if (is_resource($handle) === false) { - throw new Exception('Lock file ' . $file . ' could not be opened.'); // @codeCoverageIgnore - } + $handle = @fopen($file, 'c+b'); + if (is_resource($handle) === false) { + throw new Exception('Lock file ' . $file . ' could not be opened.'); // @codeCoverageIgnore + } - // lock the lock file exclusively to prevent changes by other threads - $result = flock($handle, LOCK_EX); - if ($result !== true) { - throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore - } + // lock the lock file exclusively to prevent changes by other threads + $result = flock($handle, LOCK_EX); + if ($result !== true) { + throw new Exception('Unexpected file system error.'); // @codeCoverageIgnore + } - return $this->handles[$file] = $handle; - } + return $this->handles[$file] = $handle; + } - /** - * Returns model ID used as the key for the data array; - * prepended with a slash because the $site otherwise won't have an ID - * - * @param \Kirby\Cms\ModelWithContent $model - * @return string - */ - public static function id(ModelWithContent $model): string - { - return '/' . $model->id(); - } + /** + * Returns model ID used as the key for the data array; + * prepended with a slash because the $site otherwise won't have an ID + * + * @param \Kirby\Cms\ModelWithContent $model + * @return string + */ + public static function id(ModelWithContent $model): string + { + return '/' . $model->id(); + } - /** - * Sets and writes the lock/unlock data for the specified model - * - * @param \Kirby\Cms\ModelWithContent $model - * @param array $data - * @return bool - * @throws \Kirby\Exception\Exception - */ - public function set(ModelWithContent $model, array $data): bool - { - $file = static::file($model); - $id = static::id($model); - $handle = $this->handle($file, true); + /** + * Sets and writes the lock/unlock data for the specified model + * + * @param \Kirby\Cms\ModelWithContent $model + * @param array $data + * @return bool + * @throws \Kirby\Exception\Exception + */ + public function set(ModelWithContent $model, array $data): bool + { + $file = static::file($model); + $id = static::id($model); + $handle = $this->handle($file, true); - $this->data[$file][$id] = $data; + $this->data[$file][$id] = $data; - // make sure to unset model id entries, - // if no lock data for the model exists - foreach ($this->data[$file] as $id => $data) { - // there is no data for that model whatsoever - if ( - isset($data['lock']) === false && - (isset($data['unlock']) === false || - count($data['unlock']) === 0) - ) { - unset($this->data[$file][$id]); + // make sure to unset model id entries, + // if no lock data for the model exists + foreach ($this->data[$file] as $id => $data) { + // there is no data for that model whatsoever + if ( + isset($data['lock']) === false && + (isset($data['unlock']) === false || + count($data['unlock']) === 0) + ) { + unset($this->data[$file][$id]); - // there is empty unlock data, but still lock data - } elseif ( - isset($data['unlock']) === true && - count($data['unlock']) === 0 - ) { - unset($this->data[$file][$id]['unlock']); - } - } + // there is empty unlock data, but still lock data + } elseif ( + isset($data['unlock']) === true && + count($data['unlock']) === 0 + ) { + unset($this->data[$file][$id]['unlock']); + } + } - // there is no data left in the file whatsoever, delete the file - if (count($this->data[$file]) === 0) { - unset($this->data[$file]); + // there is no data left in the file whatsoever, delete the file + if (count($this->data[$file]) === 0) { + unset($this->data[$file]); - // close the file handle, otherwise we can't delete it on Windows - $this->closeHandle($file); + // close the file handle, otherwise we can't delete it on Windows + $this->closeHandle($file); - return F::remove($file); - } + return F::remove($file); + } - $yaml = Data::encode($this->data[$file], 'yaml'); + $yaml = Data::encode($this->data[$file], 'yaml'); - // delete all file contents first - if (rewind($handle) !== true || ftruncate($handle, 0) !== true) { - throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore - } + // delete all file contents first + if (rewind($handle) !== true || ftruncate($handle, 0) !== true) { + throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore + } - // write the new contents - $result = fwrite($handle, $yaml); - if (is_int($result) === false || $result === 0) { - throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore - } + // write the new contents + $result = fwrite($handle, $yaml); + if (is_int($result) === false || $result === 0) { + throw new Exception('Could not write lock file ' . $file . '.'); // @codeCoverageIgnore + } - return true; - } + return true; + } } diff --git a/kirby/src/Cms/ContentTranslation.php b/kirby/src/Cms/ContentTranslation.php index 273df6a..5ffa744 100644 --- a/kirby/src/Cms/ContentTranslation.php +++ b/kirby/src/Cms/ContentTranslation.php @@ -17,226 +17,232 @@ use Kirby\Toolkit\Properties; */ class ContentTranslation { - use Properties; + use Properties; - /** - * @var string - */ - protected $code; + /** + * @var string + */ + protected $code; - /** - * @var array - */ - protected $content; + /** + * @var array + */ + protected $content; - /** - * @var string - */ - protected $contentFile; + /** + * @var string + */ + protected $contentFile; - /** - * @var Model - */ - protected $parent; + /** + * @var Model + */ + protected $parent; - /** - * @var string - */ - protected $slug; + /** + * @var string + */ + protected $slug; - /** - * Creates a new translation object - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setRequiredProperties($props, ['parent', 'code']); - $this->setOptionalProperties($props, ['slug', 'content']); - } + /** + * Creates a new translation object + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setRequiredProperties($props, ['parent', 'code']); + $this->setOptionalProperties($props, ['slug', 'content']); + } - /** - * Improve `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improve `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * Returns the language code of the - * translation - * - * @return string - */ - public function code(): string - { - return $this->code; - } + /** + * Returns the language code of the + * translation + * + * @return string + */ + public function code(): string + { + return $this->code; + } - /** - * Returns the translation content - * as plain array - * - * @return array - */ - public function content(): array - { - $parent = $this->parent(); + /** + * Returns the translation content + * as plain array + * + * @return array + */ + public function content(): array + { + $parent = $this->parent(); - if ($this->content === null) { - $this->content = $parent->readContent($this->code()); - } + if ($this->content === null) { + $this->content = $parent->readContent($this->code()); + } - $content = $this->content; + $content = $this->content; - // merge with the default content - if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) { - $default = []; + // merge with the default content + if ($this->isDefault() === false && $defaultLanguage = $parent->kirby()->defaultLanguage()) { + $default = []; - if ($defaultTranslation = $parent->translation($defaultLanguage->code())) { - $default = $defaultTranslation->content(); - } + if ($defaultTranslation = $parent->translation($defaultLanguage->code())) { + $default = $defaultTranslation->content(); + } - $content = array_merge($default, $content); - } + $content = array_merge($default, $content); + } - return $content; - } + return $content; + } - /** - * Absolute path to the translation content file - * - * @return string - */ - public function contentFile(): string - { - return $this->contentFile = $this->parent->contentFile($this->code, true); - } + /** + * Absolute path to the translation content file + * + * @return string + */ + public function contentFile(): string + { + return $this->contentFile = $this->parent->contentFile($this->code, true); + } - /** - * Checks if the translation file exists - * - * @return bool - */ - public function exists(): bool - { - return file_exists($this->contentFile()) === true; - } + /** + * Checks if the translation file exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->contentFile()) === true; + } - /** - * Returns the translation code as id - * - * @return string - */ - public function id(): string - { - return $this->code(); - } + /** + * Returns the translation code as id + * + * @return string + */ + public function id(): string + { + return $this->code(); + } - /** - * Checks if the this is the default translation - * of the model - * - * @return bool - */ - public function isDefault(): bool - { - if ($defaultLanguage = $this->parent->kirby()->defaultLanguage()) { - return $this->code() === $defaultLanguage->code(); - } + /** + * Checks if the this is the default translation + * of the model + * + * @return bool + */ + public function isDefault(): bool + { + if ($defaultLanguage = $this->parent->kirby()->defaultLanguage()) { + return $this->code() === $defaultLanguage->code(); + } - return false; - } + return false; + } - /** - * Returns the parent page, file or site object - * - * @return \Kirby\Cms\Model - */ - public function parent() - { - return $this->parent; - } + /** + * Returns the parent page, file or site object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } - /** - * @param string $code - * @return $this - */ - protected function setCode(string $code) - { - $this->code = $code; - return $this; - } + /** + * @param string $code + * @return $this + */ + protected function setCode(string $code) + { + $this->code = $code; + return $this; + } - /** - * @param array|null $content - * @return $this - */ - protected function setContent(array $content = null) - { - $this->content = $content; - return $this; - } + /** + * @param array|null $content + * @return $this + */ + protected function setContent(array $content = null) + { + if ($content !== null) { + $this->content = array_change_key_case($content); + } else { + $this->content = null; + } - /** - * @param \Kirby\Cms\Model $parent - * @return $this - */ - protected function setParent(Model $parent) - { - $this->parent = $parent; - return $this; - } + return $this; + } - /** - * @param string|null $slug - * @return $this - */ - protected function setSlug(string $slug = null) - { - $this->slug = $slug; - return $this; - } + /** + * @param \Kirby\Cms\Model $parent + * @return $this + */ + protected function setParent(Model $parent) + { + $this->parent = $parent; + return $this; + } - /** - * Returns the custom translation slug - * - * @return string|null - */ - public function slug(): ?string - { - return $this->slug ??= ($this->content()['slug'] ?? null); - } + /** + * @param string|null $slug + * @return $this + */ + protected function setSlug(string $slug = null) + { + $this->slug = $slug; + return $this; + } - /** - * Merge the old and new data - * - * @param array|null $data - * @param bool $overwrite - * @return $this - */ - public function update(array $data = null, bool $overwrite = false) - { - $this->content = $overwrite === true ? (array)$data : array_merge($this->content(), (array)$data); - return $this; - } + /** + * Returns the custom translation slug + * + * @return string|null + */ + public function slug(): ?string + { + return $this->slug ??= ($this->content()['slug'] ?? null); + } - /** - * Converts the most important translation - * props to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'code' => $this->code(), - 'content' => $this->content(), - 'exists' => $this->exists(), - 'slug' => $this->slug(), - ]; - } + /** + * Merge the old and new data + * + * @param array|null $data + * @param bool $overwrite + * @return $this + */ + public function update(array $data = null, bool $overwrite = false) + { + $data = array_change_key_case((array)$data); + $this->content = $overwrite === true ? $data : array_merge($this->content(), $data); + return $this; + } + + /** + * Converts the most important translation + * props to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'code' => $this->code(), + 'content' => $this->content(), + 'exists' => $this->exists(), + 'slug' => $this->slug(), + ]; + } } diff --git a/kirby/src/Cms/Core.php b/kirby/src/Cms/Core.php index 74c004d..48ce72e 100644 --- a/kirby/src/Cms/Core.php +++ b/kirby/src/Cms/Core.php @@ -21,452 +21,458 @@ namespace Kirby\Cms; */ class Core { - /** - * @var array - */ - protected $cache = []; + /** + * @var array + */ + protected $cache = []; - /** - * @var \Kirby\Cms\App - */ - protected $kirby; + /** + * @var \Kirby\Cms\App + */ + protected $kirby; - /** - * @var string - */ - protected $root; + /** + * @var string + */ + protected $root; - /** - * @param \Kirby\Cms\App $kirby - */ - public function __construct(App $kirby) - { - $this->kirby = $kirby; - $this->root = dirname(__DIR__, 2) . '/config'; - } + /** + * @param \Kirby\Cms\App $kirby + */ + public function __construct(App $kirby) + { + $this->kirby = $kirby; + $this->root = dirname(__DIR__, 2) . '/config'; + } - /** - * Fetches the definition array of a particular area. - * - * This is a shortcut for `$kirby->core()->load()->area()` - * to give faster access to original area code in plugins. - * - * @param string $name - * @return array|null - */ - public function area(string $name): ?array - { - return $this->load()->area($name); - } + /** + * Fetches the definition array of a particular area. + * + * This is a shortcut for `$kirby->core()->load()->area()` + * to give faster access to original area code in plugins. + * + * @param string $name + * @return array|null + */ + public function area(string $name): ?array + { + return $this->load()->area($name); + } - /** - * Returns a list of all paths to area definition files - * - * They are located in `/kirby/config/areas` - * - * @return array - */ - public function areas(): array - { - return [ - 'account' => $this->root . '/areas/account.php', - 'installation' => $this->root . '/areas/installation.php', - 'languages' => $this->root . '/areas/languages.php', - 'login' => $this->root . '/areas/login.php', - 'site' => $this->root . '/areas/site.php', - 'system' => $this->root . '/areas/system.php', - 'users' => $this->root . '/areas/users.php', - ]; - } + /** + * Returns a list of all paths to area definition files + * + * They are located in `/kirby/config/areas` + * + * @return array + */ + public function areas(): array + { + return [ + 'account' => $this->root . '/areas/account.php', + 'installation' => $this->root . '/areas/installation.php', + 'languages' => $this->root . '/areas/languages.php', + 'login' => $this->root . '/areas/login.php', + 'logout' => $this->root . '/areas/logout.php', + 'site' => $this->root . '/areas/site.php', + 'system' => $this->root . '/areas/system.php', + 'users' => $this->root . '/areas/users.php', + ]; + } - /** - * Returns a list of all default auth challenge classes - * - * @return array - */ - public function authChallenges(): array - { - return [ - 'email' => 'Kirby\Cms\Auth\EmailChallenge' - ]; - } + /** + * Returns a list of all default auth challenge classes + * + * @return array + */ + public function authChallenges(): array + { + return [ + 'email' => 'Kirby\Cms\Auth\EmailChallenge' + ]; + } - /** - * Returns a list of all paths to blueprint presets - * - * They are located in `/kirby/config/presets` - * - * @return array - */ - public function blueprintPresets(): array - { - return [ - 'pages' => $this->root . '/presets/pages.php', - 'page' => $this->root . '/presets/page.php', - 'files' => $this->root . '/presets/files.php', - ]; - } + /** + * Returns a list of all paths to blueprint presets + * + * They are located in `/kirby/config/presets` + * + * @return array + */ + public function blueprintPresets(): array + { + return [ + 'pages' => $this->root . '/presets/pages.php', + 'page' => $this->root . '/presets/page.php', + 'files' => $this->root . '/presets/files.php', + ]; + } - /** - * Returns a list of all paths to core blueprints - * - * They are located in `/kirby/config/blueprints`. - * Block blueprints are located in `/kirby/config/blocks` - * - * @return array - */ - public function blueprints(): array - { - return [ - // blocks - 'blocks/code' => $this->root . '/blocks/code/code.yml', - 'blocks/gallery' => $this->root . '/blocks/gallery/gallery.yml', - 'blocks/heading' => $this->root . '/blocks/heading/heading.yml', - 'blocks/image' => $this->root . '/blocks/image/image.yml', - 'blocks/line' => $this->root . '/blocks/line/line.yml', - 'blocks/list' => $this->root . '/blocks/list/list.yml', - 'blocks/markdown' => $this->root . '/blocks/markdown/markdown.yml', - 'blocks/quote' => $this->root . '/blocks/quote/quote.yml', - 'blocks/table' => $this->root . '/blocks/table/table.yml', - 'blocks/text' => $this->root . '/blocks/text/text.yml', - 'blocks/video' => $this->root . '/blocks/video/video.yml', + /** + * Returns a list of all paths to core blueprints + * + * They are located in `/kirby/config/blueprints`. + * Block blueprints are located in `/kirby/config/blocks` + * + * @return array + */ + public function blueprints(): array + { + return [ + // blocks + 'blocks/code' => $this->root . '/blocks/code/code.yml', + 'blocks/gallery' => $this->root . '/blocks/gallery/gallery.yml', + 'blocks/heading' => $this->root . '/blocks/heading/heading.yml', + 'blocks/image' => $this->root . '/blocks/image/image.yml', + 'blocks/line' => $this->root . '/blocks/line/line.yml', + 'blocks/list' => $this->root . '/blocks/list/list.yml', + 'blocks/markdown' => $this->root . '/blocks/markdown/markdown.yml', + 'blocks/quote' => $this->root . '/blocks/quote/quote.yml', + 'blocks/table' => $this->root . '/blocks/table/table.yml', + 'blocks/text' => $this->root . '/blocks/text/text.yml', + 'blocks/video' => $this->root . '/blocks/video/video.yml', - // file blueprints - 'files/default' => $this->root . '/blueprints/files/default.yml', + // file blueprints + 'files/default' => $this->root . '/blueprints/files/default.yml', - // page blueprints - 'pages/default' => $this->root . '/blueprints/pages/default.yml', + // page blueprints + 'pages/default' => $this->root . '/blueprints/pages/default.yml', - // site blueprints - 'site' => $this->root . '/blueprints/site.yml' - ]; - } + // site blueprints + 'site' => $this->root . '/blueprints/site.yml' + ]; + } - /** - * Returns a list of all cache driver classes - * - * @return array - */ - public function cacheTypes(): array - { - return [ - 'apcu' => 'Kirby\Cache\ApcuCache', - 'file' => 'Kirby\Cache\FileCache', - 'memcached' => 'Kirby\Cache\MemCached', - 'memory' => 'Kirby\Cache\MemoryCache', - ]; - } + /** + * Returns a list of all cache driver classes + * + * @return array + */ + public function cacheTypes(): array + { + return [ + 'apcu' => 'Kirby\Cache\ApcuCache', + 'file' => 'Kirby\Cache\FileCache', + 'memcached' => 'Kirby\Cache\MemCached', + 'memory' => 'Kirby\Cache\MemoryCache', + ]; + } - /** - * Returns an array of all core component functions - * - * The component functions can be found in - * `/kirby/config/components.php` - * - * @return array - */ - public function components(): array - { - return $this->cache['components'] ??= include $this->root . '/components.php'; - } + /** + * Returns an array of all core component functions + * + * The component functions can be found in + * `/kirby/config/components.php` + * + * @return array + */ + public function components(): array + { + return $this->cache['components'] ??= include $this->root . '/components.php'; + } - /** - * Returns a map of all field method aliases - * - * @return array - */ - public function fieldMethodAliases(): array - { - return [ - 'bool' => 'toBool', - 'esc' => 'escape', - 'excerpt' => 'toExcerpt', - 'float' => 'toFloat', - 'h' => 'html', - 'int' => 'toInt', - 'kt' => 'kirbytext', - 'kti' => 'kirbytextinline', - 'link' => 'toLink', - 'md' => 'markdown', - 'sp' => 'smartypants', - 'v' => 'isValid', - 'x' => 'xml' - ]; - } + /** + * Returns a map of all field method aliases + * + * @return array + */ + public function fieldMethodAliases(): array + { + return [ + 'bool' => 'toBool', + 'esc' => 'escape', + 'excerpt' => 'toExcerpt', + 'float' => 'toFloat', + 'h' => 'html', + 'int' => 'toInt', + 'kt' => 'kirbytext', + 'kti' => 'kirbytextinline', + 'link' => 'toLink', + 'md' => 'markdown', + 'sp' => 'smartypants', + 'v' => 'isValid', + 'x' => 'xml' + ]; + } - /** - * Returns an array of all field method functions - * - * Field methods are stored in `/kirby/config/methods.php` - * - * @return array - */ - public function fieldMethods(): array - { - return $this->cache['fieldMethods'] ??= (include $this->root . '/methods.php')($this->kirby); - } + /** + * Returns an array of all field method functions + * + * Field methods are stored in `/kirby/config/methods.php` + * + * @return array + */ + public function fieldMethods(): array + { + return $this->cache['fieldMethods'] ??= (include $this->root . '/methods.php')($this->kirby); + } - /** - * Returns an array of paths for field mixins - * - * They are located in `/kirby/config/fields/mixins` - * - * @return array - */ - public function fieldMixins(): array - { - return [ - 'datetime' => $this->root . '/fields/mixins/datetime.php', - 'filepicker' => $this->root . '/fields/mixins/filepicker.php', - 'layout' => $this->root . '/fields/mixins/layout.php', - 'min' => $this->root . '/fields/mixins/min.php', - 'options' => $this->root . '/fields/mixins/options.php', - 'pagepicker' => $this->root . '/fields/mixins/pagepicker.php', - 'picker' => $this->root . '/fields/mixins/picker.php', - 'upload' => $this->root . '/fields/mixins/upload.php', - 'userpicker' => $this->root . '/fields/mixins/userpicker.php', - ]; - } + /** + * Returns an array of paths for field mixins + * + * They are located in `/kirby/config/fields/mixins` + * + * @return array + */ + public function fieldMixins(): array + { + return [ + 'datetime' => $this->root . '/fields/mixins/datetime.php', + 'filepicker' => $this->root . '/fields/mixins/filepicker.php', + 'layout' => $this->root . '/fields/mixins/layout.php', + 'min' => $this->root . '/fields/mixins/min.php', + 'options' => $this->root . '/fields/mixins/options.php', + 'pagepicker' => $this->root . '/fields/mixins/pagepicker.php', + 'picker' => $this->root . '/fields/mixins/picker.php', + 'upload' => $this->root . '/fields/mixins/upload.php', + 'userpicker' => $this->root . '/fields/mixins/userpicker.php', + ]; + } - /** - * Returns an array of all paths and class names of panel fields - * - * Traditional panel fields are located in `/kirby/config/fields` - * - * The more complex field classes can be found in - * `/kirby/src/Form/Fields` - * - * @return array - */ - public function fields(): array - { - return [ - 'blocks' => 'Kirby\Form\Field\BlocksField', - 'checkboxes' => $this->root . '/fields/checkboxes.php', - 'date' => $this->root . '/fields/date.php', - 'email' => $this->root . '/fields/email.php', - 'files' => $this->root . '/fields/files.php', - 'gap' => $this->root . '/fields/gap.php', - 'headline' => $this->root . '/fields/headline.php', - 'hidden' => $this->root . '/fields/hidden.php', - 'info' => $this->root . '/fields/info.php', - 'layout' => 'Kirby\Form\Field\LayoutField', - 'line' => $this->root . '/fields/line.php', - 'list' => $this->root . '/fields/list.php', - 'multiselect' => $this->root . '/fields/multiselect.php', - 'number' => $this->root . '/fields/number.php', - 'pages' => $this->root . '/fields/pages.php', - 'radio' => $this->root . '/fields/radio.php', - 'range' => $this->root . '/fields/range.php', - 'select' => $this->root . '/fields/select.php', - 'slug' => $this->root . '/fields/slug.php', - 'structure' => $this->root . '/fields/structure.php', - 'tags' => $this->root . '/fields/tags.php', - 'tel' => $this->root . '/fields/tel.php', - 'text' => $this->root . '/fields/text.php', - 'textarea' => $this->root . '/fields/textarea.php', - 'time' => $this->root . '/fields/time.php', - 'toggle' => $this->root . '/fields/toggle.php', - 'url' => $this->root . '/fields/url.php', - 'users' => $this->root . '/fields/users.php', - 'writer' => $this->root . '/fields/writer.php' - ]; - } + /** + * Returns an array of all paths and class names of panel fields + * + * Traditional panel fields are located in `/kirby/config/fields` + * + * The more complex field classes can be found in + * `/kirby/src/Form/Fields` + * + * @return array + */ + public function fields(): array + { + return [ + 'blocks' => 'Kirby\Form\Field\BlocksField', + 'checkboxes' => $this->root . '/fields/checkboxes.php', + 'date' => $this->root . '/fields/date.php', + 'email' => $this->root . '/fields/email.php', + 'files' => $this->root . '/fields/files.php', + 'gap' => $this->root . '/fields/gap.php', + 'headline' => $this->root . '/fields/headline.php', + 'hidden' => $this->root . '/fields/hidden.php', + 'info' => $this->root . '/fields/info.php', + 'layout' => 'Kirby\Form\Field\LayoutField', + 'line' => $this->root . '/fields/line.php', + 'list' => $this->root . '/fields/list.php', + 'multiselect' => $this->root . '/fields/multiselect.php', + 'number' => $this->root . '/fields/number.php', + 'pages' => $this->root . '/fields/pages.php', + 'radio' => $this->root . '/fields/radio.php', + 'range' => $this->root . '/fields/range.php', + 'select' => $this->root . '/fields/select.php', + 'slug' => $this->root . '/fields/slug.php', + 'structure' => $this->root . '/fields/structure.php', + 'tags' => $this->root . '/fields/tags.php', + 'tel' => $this->root . '/fields/tel.php', + 'text' => $this->root . '/fields/text.php', + 'textarea' => $this->root . '/fields/textarea.php', + 'time' => $this->root . '/fields/time.php', + 'toggle' => $this->root . '/fields/toggle.php', + 'toggles' => $this->root . '/fields/toggles.php', + 'url' => $this->root . '/fields/url.php', + 'users' => $this->root . '/fields/users.php', + 'writer' => $this->root . '/fields/writer.php' + ]; + } - /** - * Returns a map of all kirbytag aliases - * - * @return array - */ - public function kirbyTagAliases(): array - { - return [ - 'youtube' => 'video', - 'vimeo' => 'video' - ]; - } + /** + * Returns a map of all kirbytag aliases + * + * @return array + */ + public function kirbyTagAliases(): array + { + return [ + 'youtube' => 'video', + 'vimeo' => 'video' + ]; + } - /** - * Returns an array of all kirbytag definitions - * - * They are located in `/kirby/config/tags.php` - * - * @return array - */ - public function kirbyTags(): array - { - return $this->cache['kirbytags'] ??= include $this->root . '/tags.php'; - } + /** + * Returns an array of all kirbytag definitions + * + * They are located in `/kirby/config/tags.php` + * + * @return array + */ + public function kirbyTags(): array + { + return $this->cache['kirbytags'] ??= include $this->root . '/tags.php'; + } - /** - * Loads a core part of Kirby - * - * The loader is set to not include plugins. - * This way, you can access original Kirby core code - * through this load method. - * - * @return \Kirby\Cms\Loader - */ - public function load() - { - return new Loader($this->kirby, false); - } + /** + * Loads a core part of Kirby + * + * The loader is set to not include plugins. + * This way, you can access original Kirby core code + * through this load method. + * + * @return \Kirby\Cms\Loader + */ + public function load() + { + return new Loader($this->kirby, false); + } - /** - * Returns all absolute paths to important directories - * - * Roots are resolved and baked in `\Kirby\Cms\App::bakeRoots()` - * - * @return array - */ - public function roots(): array - { - return $this->cache['roots'] ??= [ - 'kirby' => fn (array $roots) => dirname(__DIR__, 2), - 'i18n' => fn (array $roots) => $roots['kirby'] . '/i18n', - 'i18n:translations' => fn (array $roots) => $roots['i18n'] . '/translations', - 'i18n:rules' => fn (array $roots) => $roots['i18n'] . '/rules', + /** + * Returns all absolute paths to important directories + * + * Roots are resolved and baked in `\Kirby\Cms\App::bakeRoots()` + * + * @return array + */ + public function roots(): array + { + return $this->cache['roots'] ??= [ + 'kirby' => fn (array $roots) => dirname(__DIR__, 2), + 'i18n' => fn (array $roots) => $roots['kirby'] . '/i18n', + 'i18n:translations' => fn (array $roots) => $roots['i18n'] . '/translations', + 'i18n:rules' => fn (array $roots) => $roots['i18n'] . '/rules', - 'index' => fn (array $roots) => dirname(__DIR__, 3), - 'assets' => fn (array $roots) => $roots['index'] . '/assets', - 'content' => fn (array $roots) => $roots['index'] . '/content', - 'media' => fn (array $roots) => $roots['index'] . '/media', - 'panel' => fn (array $roots) => $roots['kirby'] . '/panel', - 'site' => fn (array $roots) => $roots['index'] . '/site', - 'accounts' => fn (array $roots) => $roots['site'] . '/accounts', - 'blueprints' => fn (array $roots) => $roots['site'] . '/blueprints', - 'cache' => fn (array $roots) => $roots['site'] . '/cache', - 'collections' => fn (array $roots) => $roots['site'] . '/collections', - 'config' => fn (array $roots) => $roots['site'] . '/config', - 'controllers' => fn (array $roots) => $roots['site'] . '/controllers', - 'languages' => fn (array $roots) => $roots['site'] . '/languages', - 'license' => fn (array $roots) => $roots['config'] . '/.license', - 'logs' => fn (array $roots) => $roots['site'] . '/logs', - 'models' => fn (array $roots) => $roots['site'] . '/models', - 'plugins' => fn (array $roots) => $roots['site'] . '/plugins', - 'sessions' => fn (array $roots) => $roots['site'] . '/sessions', - 'snippets' => fn (array $roots) => $roots['site'] . '/snippets', - 'templates' => fn (array $roots) => $roots['site'] . '/templates', - 'roles' => fn (array $roots) => $roots['blueprints'] . '/users', - ]; - } + 'index' => fn (array $roots) => dirname(__DIR__, 3), + 'assets' => fn (array $roots) => $roots['index'] . '/assets', + 'content' => fn (array $roots) => $roots['index'] . '/content', + 'media' => fn (array $roots) => $roots['index'] . '/media', + 'panel' => fn (array $roots) => $roots['kirby'] . '/panel', + 'site' => fn (array $roots) => $roots['index'] . '/site', + 'accounts' => fn (array $roots) => $roots['site'] . '/accounts', + 'blueprints' => fn (array $roots) => $roots['site'] . '/blueprints', + 'cache' => fn (array $roots) => $roots['site'] . '/cache', + 'collections' => fn (array $roots) => $roots['site'] . '/collections', + 'config' => fn (array $roots) => $roots['site'] . '/config', + 'controllers' => fn (array $roots) => $roots['site'] . '/controllers', + 'languages' => fn (array $roots) => $roots['site'] . '/languages', + 'license' => fn (array $roots) => $roots['config'] . '/.license', + 'logs' => fn (array $roots) => $roots['site'] . '/logs', + 'models' => fn (array $roots) => $roots['site'] . '/models', + 'plugins' => fn (array $roots) => $roots['site'] . '/plugins', + 'sessions' => fn (array $roots) => $roots['site'] . '/sessions', + 'snippets' => fn (array $roots) => $roots['site'] . '/snippets', + 'templates' => fn (array $roots) => $roots['site'] . '/templates', + 'roles' => fn (array $roots) => $roots['blueprints'] . '/users', + ]; + } - /** - * Returns an array of all routes for Kirby’s router - * - * Routes are split into `before` and `after` routes. - * - * Plugin routes will be injected inbetween. - * - * @return array - */ - public function routes(): array - { - return $this->cache['routes'] ??= (include $this->root . '/routes.php')($this->kirby); - } + /** + * Returns an array of all routes for Kirby’s router + * + * Routes are split into `before` and `after` routes. + * + * Plugin routes will be injected inbetween. + * + * @return array + */ + public function routes(): array + { + return $this->cache['routes'] ??= (include $this->root . '/routes.php')($this->kirby); + } - /** - * Returns a list of all paths to core block snippets - * - * They are located in `/kirby/config/blocks` - * - * @return array - */ - public function snippets(): array - { - return [ - 'blocks/code' => $this->root . '/blocks/code/code.php', - 'blocks/gallery' => $this->root . '/blocks/gallery/gallery.php', - 'blocks/heading' => $this->root . '/blocks/heading/heading.php', - 'blocks/image' => $this->root . '/blocks/image/image.php', - 'blocks/line' => $this->root . '/blocks/line/line.php', - 'blocks/list' => $this->root . '/blocks/list/list.php', - 'blocks/markdown' => $this->root . '/blocks/markdown/markdown.php', - 'blocks/quote' => $this->root . '/blocks/quote/quote.php', - 'blocks/table' => $this->root . '/blocks/table/table.php', - 'blocks/text' => $this->root . '/blocks/text/text.php', - 'blocks/video' => $this->root . '/blocks/video/video.php', - ]; - } + /** + * Returns a list of all paths to core block snippets + * + * They are located in `/kirby/config/blocks` + * + * @return array + */ + public function snippets(): array + { + return [ + 'blocks/code' => $this->root . '/blocks/code/code.php', + 'blocks/gallery' => $this->root . '/blocks/gallery/gallery.php', + 'blocks/heading' => $this->root . '/blocks/heading/heading.php', + 'blocks/image' => $this->root . '/blocks/image/image.php', + 'blocks/line' => $this->root . '/blocks/line/line.php', + 'blocks/list' => $this->root . '/blocks/list/list.php', + 'blocks/markdown' => $this->root . '/blocks/markdown/markdown.php', + 'blocks/quote' => $this->root . '/blocks/quote/quote.php', + 'blocks/table' => $this->root . '/blocks/table/table.php', + 'blocks/text' => $this->root . '/blocks/text/text.php', + 'blocks/video' => $this->root . '/blocks/video/video.php', + ]; + } - /** - * Returns a list of paths to section mixins - * - * They are located in `/kirby/config/sections/mixins` - * - * @return array - */ - public function sectionMixins(): array - { - return [ - 'empty' => $this->root . '/sections/mixins/empty.php', - 'headline' => $this->root . '/sections/mixins/headline.php', - 'help' => $this->root . '/sections/mixins/help.php', - 'layout' => $this->root . '/sections/mixins/layout.php', - 'max' => $this->root . '/sections/mixins/max.php', - 'min' => $this->root . '/sections/mixins/min.php', - 'pagination' => $this->root . '/sections/mixins/pagination.php', - 'parent' => $this->root . '/sections/mixins/parent.php', - ]; - } + /** + * Returns a list of paths to section mixins + * + * They are located in `/kirby/config/sections/mixins` + * + * @return array + */ + public function sectionMixins(): array + { + return [ + 'details' => $this->root . '/sections/mixins/details.php', + 'empty' => $this->root . '/sections/mixins/empty.php', + 'headline' => $this->root . '/sections/mixins/headline.php', + 'help' => $this->root . '/sections/mixins/help.php', + 'layout' => $this->root . '/sections/mixins/layout.php', + 'max' => $this->root . '/sections/mixins/max.php', + 'min' => $this->root . '/sections/mixins/min.php', + 'pagination' => $this->root . '/sections/mixins/pagination.php', + 'parent' => $this->root . '/sections/mixins/parent.php', + 'search' => $this->root . '/sections/mixins/search.php', + 'sort' => $this->root . '/sections/mixins/sort.php', + ]; + } - /** - * Returns a list of all section definitions - * - * They are located in `/kirby/config/sections` - * - * @return array - */ - public function sections(): array - { - return [ - 'fields' => $this->root . '/sections/fields.php', - 'files' => $this->root . '/sections/files.php', - 'info' => $this->root . '/sections/info.php', - 'pages' => $this->root . '/sections/pages.php', - ]; - } + /** + * Returns a list of all section definitions + * + * They are located in `/kirby/config/sections` + * + * @return array + */ + public function sections(): array + { + return [ + 'fields' => $this->root . '/sections/fields.php', + 'files' => $this->root . '/sections/files.php', + 'info' => $this->root . '/sections/info.php', + 'pages' => $this->root . '/sections/pages.php', + 'stats' => $this->root . '/sections/stats.php', + ]; + } - /** - * Returns a list of paths to all system templates - * - * They are located in `/kirby/config/templates` - * - * @return array - */ - public function templates(): array - { - return [ - 'emails/auth/login' => $this->root . '/templates/emails/auth/login.php', - 'emails/auth/password-reset' => $this->root . '/templates/emails/auth/password-reset.php' - ]; - } + /** + * Returns a list of paths to all system templates + * + * They are located in `/kirby/config/templates` + * + * @return array + */ + public function templates(): array + { + return [ + 'emails/auth/login' => $this->root . '/templates/emails/auth/login.php', + 'emails/auth/password-reset' => $this->root . '/templates/emails/auth/password-reset.php' + ]; + } - /** - * Returns an array with all system URLs - * - * URLs are resolved and baked in `\Kirby\Cms\App::bakeUrls()` - * - * @return array - */ - public function urls(): array - { - return $this->cache['urls'] ??= [ - 'index' => fn () => $this->kirby->environment()->url(), - 'base' => fn (array $urls) => rtrim($urls['index'], '/'), - 'current' => function (array $urls) { - $path = trim($this->kirby->path(), '/'); + /** + * Returns an array with all system URLs + * + * URLs are resolved and baked in `\Kirby\Cms\App::bakeUrls()` + * + * @return array + */ + public function urls(): array + { + return $this->cache['urls'] ??= [ + 'index' => fn () => $this->kirby->environment()->baseUrl(), + 'base' => fn (array $urls) => rtrim($urls['index'], '/'), + 'current' => function (array $urls) { + $path = trim($this->kirby->path(), '/'); - if (empty($path) === true) { - return $urls['index']; - } else { - return $urls['base'] . '/' . $path; - } - }, - 'assets' => fn (array $urls) => $urls['base'] . '/assets', - 'api' => fn (array $urls) => $urls['base'] . '/' . $this->kirby->option('api.slug', 'api'), - 'media' => fn (array $urls) => $urls['base'] . '/media', - 'panel' => fn (array $urls) => $urls['base'] . '/' . $this->kirby->option('panel.slug', 'panel') - ]; - } + if (empty($path) === true) { + return $urls['index']; + } else { + return $urls['base'] . '/' . $path; + } + }, + 'assets' => fn (array $urls) => $urls['base'] . '/assets', + 'api' => fn (array $urls) => $urls['base'] . '/' . $this->kirby->option('api.slug', 'api'), + 'media' => fn (array $urls) => $urls['base'] . '/media', + 'panel' => fn (array $urls) => $urls['base'] . '/' . $this->kirby->option('panel.slug', 'panel') + ]; + } } diff --git a/kirby/src/Cms/Email.php b/kirby/src/Cms/Email.php index 8943a84..6009792 100644 --- a/kirby/src/Cms/Email.php +++ b/kirby/src/Cms/Email.php @@ -19,237 +19,236 @@ use Kirby\Exception\NotFoundException; */ class Email { - /** - * Options configured through the `email` CMS option - * - * @var array - */ - protected $options; + /** + * Options configured through the `email` CMS option + * + * @var array + */ + protected $options; - /** - * Props for the email object; will be passed to the - * Kirby\Email\Email class - * - * @var array - */ - protected $props; + /** + * Props for the email object; will be passed to the + * Kirby\Email\Email class + * + * @var array + */ + protected $props; - /** - * Class constructor - * - * @param string|array $preset Preset name from the config or a simple props array - * @param array $props Props array to override the $preset - */ - public function __construct($preset = [], array $props = []) - { - $this->options = App::instance()->option('email'); + /** + * Class constructor + * + * @param string|array $preset Preset name from the config or a simple props array + * @param array $props Props array to override the $preset + */ + public function __construct($preset = [], array $props = []) + { + $this->options = App::instance()->option('email'); - // build a prop array based on preset and props - $preset = $this->preset($preset); - $this->props = array_merge($preset, $props); + // build a prop array based on preset and props + $preset = $this->preset($preset); + $this->props = array_merge($preset, $props); - // add transport settings - if (isset($this->props['transport']) === false) { - $this->props['transport'] = $this->options['transport'] ?? []; - } + // add transport settings + if (isset($this->props['transport']) === false) { + $this->props['transport'] = $this->options['transport'] ?? []; + } - // add predefined beforeSend option - if (isset($this->props['beforeSend']) === false) { - $this->props['beforeSend'] = $this->options['beforeSend'] ?? null; - } + // add predefined beforeSend option + if (isset($this->props['beforeSend']) === false) { + $this->props['beforeSend'] = $this->options['beforeSend'] ?? null; + } - // transform model objects to values - $this->transformUserSingle('from', 'fromName'); - $this->transformUserSingle('replyTo', 'replyToName'); - $this->transformUserMultiple('to'); - $this->transformUserMultiple('cc'); - $this->transformUserMultiple('bcc'); - $this->transformFile('attachments'); + // transform model objects to values + $this->transformUserSingle('from', 'fromName'); + $this->transformUserSingle('replyTo', 'replyToName'); + $this->transformUserMultiple('to'); + $this->transformUserMultiple('cc'); + $this->transformUserMultiple('bcc'); + $this->transformFile('attachments'); - // load template for body text - $this->template(); - } + // load template for body text + $this->template(); + } - /** - * Grabs a preset from the options; supports fixed - * prop arrays in case a preset is not needed - * - * @param string|array $preset Preset name or simple prop array - * @return array - * @throws \Kirby\Exception\NotFoundException - */ - protected function preset($preset): array - { - // only passed props, not preset name - if (is_array($preset) === true) { - return $preset; - } + /** + * Grabs a preset from the options; supports fixed + * prop arrays in case a preset is not needed + * + * @param string|array $preset Preset name or simple prop array + * @return array + * @throws \Kirby\Exception\NotFoundException + */ + protected function preset($preset): array + { + // only passed props, not preset name + if (is_array($preset) === true) { + return $preset; + } - // preset does not exist - if (isset($this->options['presets'][$preset]) !== true) { - throw new NotFoundException([ - 'key' => 'email.preset.notFound', - 'data' => ['name' => $preset] - ]); - } + // preset does not exist + if (isset($this->options['presets'][$preset]) !== true) { + throw new NotFoundException([ + 'key' => 'email.preset.notFound', + 'data' => ['name' => $preset] + ]); + } - return $this->options['presets'][$preset]; - } + return $this->options['presets'][$preset]; + } - /** - * Renders the email template(s) and sets the body props - * to the result - * - * @return void - * @throws \Kirby\Exception\NotFoundException - */ - protected function template(): void - { - if (isset($this->props['template']) === true) { + /** + * Renders the email template(s) and sets the body props + * to the result + * + * @return void + * @throws \Kirby\Exception\NotFoundException + */ + protected function template(): void + { + if (isset($this->props['template']) === true) { + // prepare data to be passed to template + $data = $this->props['data'] ?? []; - // prepare data to be passed to template - $data = $this->props['data'] ?? []; + // check if html/text templates exist + $html = $this->getTemplate($this->props['template'], 'html'); + $text = $this->getTemplate($this->props['template'], 'text'); - // check if html/text templates exist - $html = $this->getTemplate($this->props['template'], 'html'); - $text = $this->getTemplate($this->props['template'], 'text'); + if ($html->exists()) { + $this->props['body'] = [ + 'html' => $html->render($data) + ]; - if ($html->exists()) { - $this->props['body'] = [ - 'html' => $html->render($data) - ]; + if ($text->exists()) { + $this->props['body']['text'] = $text->render($data); + } - if ($text->exists()) { - $this->props['body']['text'] = $text->render($data); - } + // fallback to single email text template + } elseif ($text->exists()) { + $this->props['body'] = $text->render($data); + } else { + throw new NotFoundException('The email template "' . $this->props['template'] . '" cannot be found'); + } + } + } - // fallback to single email text template - } elseif ($text->exists()) { - $this->props['body'] = $text->render($data); - } else { - throw new NotFoundException('The email template "' . $this->props['template'] . '" cannot be found'); - } - } - } + /** + * Returns an email template by name and type + * + * @param string $name Template name + * @param string|null $type `html` or `text` + * @return \Kirby\Cms\Template + */ + protected function getTemplate(string $name, string $type = null) + { + return App::instance()->template('emails/' . $name, $type, 'text'); + } - /** - * Returns an email template by name and type - * - * @param string $name Template name - * @param string|null $type `html` or `text` - * @return \Kirby\Cms\Template - */ - protected function getTemplate(string $name, string $type = null) - { - return App::instance()->template('emails/' . $name, $type, 'text'); - } + /** + * Returns the prop array + * + * @return array + */ + public function toArray(): array + { + return $this->props; + } - /** - * Returns the prop array - * - * @return array - */ - public function toArray(): array - { - return $this->props; - } + /** + * Transforms file object(s) to an array of file roots; + * supports simple strings, file objects or collections/arrays of either + * + * @param string $prop Prop to transform + * @return void + */ + protected function transformFile(string $prop): void + { + $this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\File', 'root'); + } - /** - * Transforms file object(s) to an array of file roots; - * supports simple strings, file objects or collections/arrays of either - * - * @param string $prop Prop to transform - * @return void - */ - protected function transformFile(string $prop): void - { - $this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\File', 'root'); - } + /** + * Transforms Kirby models to a simplified collection + * + * @param string $prop Prop to transform + * @param string $class Fully qualified class name of the supported model + * @param string $contentValue Model method that returns the array value + * @param string|null $contentKey Optional model method that returns the array key; + * returns a simple value-only array if not given + * @return array Simple key-value or just value array with the transformed prop data + */ + protected function transformModel(string $prop, string $class, string $contentValue, string $contentKey = null): array + { + $value = $this->props[$prop] ?? []; - /** - * Transforms Kirby models to a simplified collection - * - * @param string $prop Prop to transform - * @param string $class Fully qualified class name of the supported model - * @param string $contentValue Model method that returns the array value - * @param string|null $contentKey Optional model method that returns the array key; - * returns a simple value-only array if not given - * @return array Simple key-value or just value array with the transformed prop data - */ - protected function transformModel(string $prop, string $class, string $contentValue, string $contentKey = null): array - { - $value = $this->props[$prop] ?? []; + // ensure consistent input by making everything an iterable value + if (is_iterable($value) !== true) { + $value = [$value]; + } - // ensure consistent input by making everything an iterable value - if (is_iterable($value) !== true) { - $value = [$value]; - } + $result = []; + foreach ($value as $key => $item) { + if (is_string($item) === true) { + // value is already a string + if ($contentKey !== null && is_string($key) === true) { + $result[$key] = $item; + } else { + $result[] = $item; + } + } elseif (is_a($item, $class) === true) { + // value is a model object, get value through content method(s) + if ($contentKey !== null) { + $result[(string)$item->$contentKey()] = (string)$item->$contentValue(); + } else { + $result[] = (string)$item->$contentValue(); + } + } else { + // invalid input + throw new InvalidArgumentException('Invalid input for prop "' . $prop . '", expected string or "' . $class . '" object or collection'); + } + } - $result = []; - foreach ($value as $key => $item) { - if (is_string($item) === true) { - // value is already a string - if ($contentKey !== null && is_string($key) === true) { - $result[$key] = $item; - } else { - $result[] = $item; - } - } elseif (is_a($item, $class) === true) { - // value is a model object, get value through content method(s) - if ($contentKey !== null) { - $result[(string)$item->$contentKey()] = (string)$item->$contentValue(); - } else { - $result[] = (string)$item->$contentValue(); - } - } else { - // invalid input - throw new InvalidArgumentException('Invalid input for prop "' . $prop . '", expected string or "' . $class . '" object or collection'); - } - } + return $result; + } - return $result; - } + /** + * Transforms an user object to the email address and name; + * supports simple strings, user objects or collections/arrays of either + * (note: only the first item in a collection/array will be used) + * + * @param string $addressProp Prop with the email address + * @param string $nameProp Prop with the name corresponding to the $addressProp + * @return void + */ + protected function transformUserSingle(string $addressProp, string $nameProp): void + { + $result = $this->transformModel($addressProp, 'Kirby\Cms\User', 'name', 'email'); - /** - * Transforms an user object to the email address and name; - * supports simple strings, user objects or collections/arrays of either - * (note: only the first item in a collection/array will be used) - * - * @param string $addressProp Prop with the email address - * @param string $nameProp Prop with the name corresponding to the $addressProp - * @return void - */ - protected function transformUserSingle(string $addressProp, string $nameProp): void - { - $result = $this->transformModel($addressProp, 'Kirby\Cms\User', 'name', 'email'); + $address = array_keys($result)[0] ?? null; + $name = $result[$address] ?? null; - $address = array_keys($result)[0] ?? null; - $name = $result[$address] ?? null; + // if the array is non-associative, the value is the address + if (is_int($address) === true) { + $address = $name; + $name = null; + } - // if the array is non-associative, the value is the address - if (is_int($address) === true) { - $address = $name; - $name = null; - } + // always use the address as we have transformed that prop above + $this->props[$addressProp] = $address; - // always use the address as we have transformed that prop above - $this->props[$addressProp] = $address; + // only use the name from the user if no custom name was set + if (isset($this->props[$nameProp]) === false || $this->props[$nameProp] === null) { + $this->props[$nameProp] = $name; + } + } - // only use the name from the user if no custom name was set - if (isset($this->props[$nameProp]) === false || $this->props[$nameProp] === null) { - $this->props[$nameProp] = $name; - } - } - - /** - * Transforms user object(s) to the email address(es) and name(s); - * supports simple strings, user objects or collections/arrays of either - * - * @param string $prop Prop to transform - * @return void - */ - protected function transformUserMultiple(string $prop): void - { - $this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\User', 'name', 'email'); - } + /** + * Transforms user object(s) to the email address(es) and name(s); + * supports simple strings, user objects or collections/arrays of either + * + * @param string $prop Prop to transform + * @return void + */ + protected function transformUserMultiple(string $prop): void + { + $this->props[$prop] = $this->transformModel($prop, 'Kirby\Cms\User', 'name', 'email'); + } } diff --git a/kirby/src/Cms/Environment.php b/kirby/src/Cms/Environment.php deleted file mode 100644 index 0f90004..0000000 --- a/kirby/src/Cms/Environment.php +++ /dev/null @@ -1,222 +0,0 @@ - - * @link https://getkirby.com - * @copyright Bastian Allgeier - * @license https://getkirby.com/license - */ -class Environment -{ - /** - * @var string - */ - protected $root; - - /** - * @var \Kirby\Http\Uri - */ - protected $uri; - - /** - * @param string $root - * @param bool|string|array|null $allowed - */ - public function __construct(string $root, $allowed = null) - { - $this->root = $root; - - if (is_string($allowed) === true) { - $this->setupFromString($allowed); - return; - } - - if (is_array($allowed) === true) { - $this->setupFromArray($allowed); - return; - } - - if (is_int($allowed) === true) { - $this->setupFromFlag($allowed); - return; - } - - if (is_null($allowed) === true) { - $this->setupFromFlag(Server::HOST_FROM_SERVER | Server::HOST_ALLOW_EMPTY); - return; - } - - throw new InvalidArgumentException('Invalid allow list setup for base URLs'); - } - - /** - * Throw an exception if the host in the URI - * object is empty - * - * @throws \Kirby\Exception\InvalidArgumentException - * @return void - */ - protected function blockEmptyHost(): void - { - if (empty($this->uri->host()) === true) { - throw new InvalidArgumentException('Invalid host setup. The detected host is not allowed.'); - } - } - - /** - * Returns the detected host name - * - * @return string|null - */ - public function host(): ?string - { - return $this->uri->host(); - } - - /** - * Loads and returns the environment options - * - * @return array - */ - public function options(): array - { - $configHost = []; - $configAddr = []; - - $host = $this->host(); - $addr = Server::address(); - - // load the config for the host - if (empty($host) === false) { - $configHost = F::load($this->root . '/config.' . $host . '.php', []); - } - - // load the config for the server IP - if (empty($addr) === false) { - $configAddr = F::load($this->root . '/config.' . $addr . '.php', []); - } - - return array_replace_recursive($configHost, $configAddr); - } - - /** - * The current URL should be auto detected from a host allowlist - * - * @param array $allowed - * @return \Kirby\Http\Uri - */ - public function setupFromArray(array $allowed) - { - $allowedStrings = []; - $allowedUris = []; - $hosts = []; - - foreach ($allowed as $url) { - $allowedUris[] = $uri = new Uri($url, ['slash' => false]); - $allowedStrings[] = $uri->toString(); - $hosts[] = $uri->host(); - } - - // register all allowed hosts - Server::hosts($hosts); - - // get the index URL, including the subfolder if it exists - $this->uri = Uri::index(); - - // empty URLs don't make sense in an allow list - $this->blockEmptyHost(); - - // validate against the list of allowed base URLs - if (in_array($this->uri->toString(), $allowedStrings) === false) { - throw new InvalidArgumentException('The subfolder is not in the allowed base URL list'); - } - - return $this->uri; - } - - /** - * The URL option receives a set of Server constant flags - * - * Server::HOST_FROM_SERVER - * Server::HOST_FROM_SERVER | Server::HOST_ALLOW_EMPTY - * Server::HOST_FROM_HOST - * Server::HOST_FROM_HOST | Server::HOST_ALLOW_EMPTY - * - * @param int $allowed - * @return \Kirby\Http\Uri - */ - public function setupFromFlag(int $allowed) - { - // allow host detection from host headers - if ($allowed & Server::HOST_FROM_HEADER) { - Server::hosts(Server::HOST_FROM_HEADER); - - // detect host only from server name - } else { - Server::hosts(Server::HOST_FROM_SERVER); - } - - // get the base URL - $this->uri = Uri::index(); - - // accept empty hosts - if ($allowed & Server::HOST_ALLOW_EMPTY) { - return $this->uri; - } - - // block empty hosts - $this->blockEmptyHost(); - - return $this->uri; - } - - /** - * The current URL is predefined with a single string - * and not detected automatically. - * - * If the url option is relative (i.e. '/' or '/some/subfolder') - * The host will be empty and that's totally fine. - * No need to block an empty host here - * - * @param string $allowed - * @return \Kirby\Http\Uri - */ - public function setupFromString(string $allowed) - { - // create the URI object directly from the given option - // without any form of detection from the server - $this->uri = new Uri($allowed); - - // only create an allow list from absolute URLs - // otherwise the default secure host detection - // behavior will be used - if (empty($host = $this->uri->host()) === false) { - Server::hosts([$host]); - } - - return $this->uri; - } - - /** - * Returns the base URL for the environment - * - * @return string - */ - public function url(): string - { - return $this->uri; - } -} diff --git a/kirby/src/Cms/Event.php b/kirby/src/Cms/Event.php index 40d5af0..c6ed0c2 100644 --- a/kirby/src/Cms/Event.php +++ b/kirby/src/Cms/Event.php @@ -21,270 +21,270 @@ use Kirby\Toolkit\Controller; */ class Event { - /** - * The full event name - * (e.g. `page.create:after`) - * - * @var string - */ - protected $name; + /** + * The full event name + * (e.g. `page.create:after`) + * + * @var string + */ + protected $name; - /** - * The event type - * (e.g. `page` in `page.create:after`) - * - * @var string - */ - protected $type; + /** + * The event type + * (e.g. `page` in `page.create:after`) + * + * @var string + */ + protected $type; - /** - * The event action - * (e.g. `create` in `page.create:after`) - * - * @var string|null - */ - protected $action; + /** + * The event action + * (e.g. `create` in `page.create:after`) + * + * @var string|null + */ + protected $action; - /** - * The event state - * (e.g. `after` in `page.create:after`) - * - * @var string|null - */ - protected $state; + /** + * The event state + * (e.g. `after` in `page.create:after`) + * + * @var string|null + */ + protected $state; - /** - * The event arguments - * - * @var array - */ - protected $arguments = []; + /** + * The event arguments + * + * @var array + */ + protected $arguments = []; - /** - * Class constructor - * - * @param string $name Full event name - * @param array $arguments Associative array of named event arguments - */ - public function __construct(string $name, array $arguments = []) - { - // split the event name into `$type.$action:$state` - // $action and $state are optional; - // if there is more than one dot, $type will be greedy - $regex = '/^(?.+?)(?:\.(?[^.]*?))?(?:\:(?.*))?$/'; - preg_match($regex, $name, $matches, PREG_UNMATCHED_AS_NULL); + /** + * Class constructor + * + * @param string $name Full event name + * @param array $arguments Associative array of named event arguments + */ + public function __construct(string $name, array $arguments = []) + { + // split the event name into `$type.$action:$state` + // $action and $state are optional; + // if there is more than one dot, $type will be greedy + $regex = '/^(?.+?)(?:\.(?[^.]*?))?(?:\:(?.*))?$/'; + preg_match($regex, $name, $matches, PREG_UNMATCHED_AS_NULL); - $this->name = $name; - $this->type = $matches['type']; - $this->action = $matches['action'] ?? null; - $this->state = $matches['state'] ?? null; - $this->arguments = $arguments; - } + $this->name = $name; + $this->type = $matches['type']; + $this->action = $matches['action'] ?? null; + $this->state = $matches['state'] ?? null; + $this->arguments = $arguments; + } - /** - * Magic caller for event arguments - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - return $this->argument($method); - } + /** + * Magic caller for event arguments + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + return $this->argument($method); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * Makes it possible to simply echo - * or stringify the entire object - * - * @return string - */ - public function __toString(): string - { - return $this->toString(); - } + /** + * Makes it possible to simply echo + * or stringify the entire object + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } - /** - * Returns the action of the event (e.g. `create`) - * or `null` if the event name does not include an action - * - * @return string|null - */ - public function action(): ?string - { - return $this->action; - } + /** + * Returns the action of the event (e.g. `create`) + * or `null` if the event name does not include an action + * + * @return string|null + */ + public function action(): ?string + { + return $this->action; + } - /** - * Returns a specific event argument - * - * @param string $name - * @return mixed - */ - public function argument(string $name) - { - if (isset($this->arguments[$name]) === true) { - return $this->arguments[$name]; - } + /** + * Returns a specific event argument + * + * @param string $name + * @return mixed + */ + public function argument(string $name) + { + if (isset($this->arguments[$name]) === true) { + return $this->arguments[$name]; + } - return null; - } + return null; + } - /** - * Returns the arguments of the event - * - * @return array - */ - public function arguments(): array - { - return $this->arguments; - } + /** + * Returns the arguments of the event + * + * @return array + */ + public function arguments(): array + { + return $this->arguments; + } - /** - * Calls a hook with the event data and returns - * the hook's return value - * - * @param object|null $bind Optional object to bind to the hook function - * @param \Closure $hook - * @return mixed - */ - public function call(?object $bind, Closure $hook) - { - // collect the list of possible hook arguments - $data = $this->arguments(); - $data['event'] = $this; + /** + * Calls a hook with the event data and returns + * the hook's return value + * + * @param object|null $bind Optional object to bind to the hook function + * @param \Closure $hook + * @return mixed + */ + public function call(?object $bind, Closure $hook) + { + // collect the list of possible hook arguments + $data = $this->arguments(); + $data['event'] = $this; - // magically call the hook with the arguments it requested - $hook = new Controller($hook); - return $hook->call($bind, $data); - } + // magically call the hook with the arguments it requested + $hook = new Controller($hook); + return $hook->call($bind, $data); + } - /** - * Returns the full name of the event - * - * @return string - */ - public function name(): string - { - return $this->name; - } + /** + * Returns the full name of the event + * + * @return string + */ + public function name(): string + { + return $this->name; + } - /** - * Returns the full list of possible wildcard - * event names based on the current event name - * - * @return array - */ - public function nameWildcards(): array - { - // if the event is already a wildcard event, no further variation is possible - if ($this->type === '*' || $this->action === '*' || $this->state === '*') { - return []; - } + /** + * Returns the full list of possible wildcard + * event names based on the current event name + * + * @return array + */ + public function nameWildcards(): array + { + // if the event is already a wildcard event, no further variation is possible + if ($this->type === '*' || $this->action === '*' || $this->state === '*') { + return []; + } - if ($this->action !== null && $this->state !== null) { - // full $type.$action:$state event + if ($this->action !== null && $this->state !== null) { + // full $type.$action:$state event - return [ - $this->type . '.*:' . $this->state, - $this->type . '.' . $this->action . ':*', - $this->type . '.*:*', - '*.' . $this->action . ':' . $this->state, - '*.' . $this->action . ':*', - '*:' . $this->state, - '*' - ]; - } elseif ($this->state !== null) { - // event without action: $type:$state + return [ + $this->type . '.*:' . $this->state, + $this->type . '.' . $this->action . ':*', + $this->type . '.*:*', + '*.' . $this->action . ':' . $this->state, + '*.' . $this->action . ':*', + '*:' . $this->state, + '*' + ]; + } elseif ($this->state !== null) { + // event without action: $type:$state - return [ - $this->type . ':*', - '*:' . $this->state, - '*' - ]; - } elseif ($this->action !== null) { - // event without state: $type.$action + return [ + $this->type . ':*', + '*:' . $this->state, + '*' + ]; + } elseif ($this->action !== null) { + // event without state: $type.$action - return [ - $this->type . '.*', - '*.' . $this->action, - '*' - ]; - } else { - // event with a simple name + return [ + $this->type . '.*', + '*.' . $this->action, + '*' + ]; + } else { + // event with a simple name - return ['*']; - } - } + return ['*']; + } + } - /** - * Returns the state of the event (e.g. `after`) - * - * @return string|null - */ - public function state(): ?string - { - return $this->state; - } + /** + * Returns the state of the event (e.g. `after`) + * + * @return string|null + */ + public function state(): ?string + { + return $this->state; + } - /** - * Returns the event data as array - * - * @return array - */ - public function toArray(): array - { - return [ - 'name' => $this->name, - 'arguments' => $this->arguments - ]; - } + /** + * Returns the event data as array + * + * @return array + */ + public function toArray(): array + { + return [ + 'name' => $this->name, + 'arguments' => $this->arguments + ]; + } - /** - * Returns the event name as string - * - * @return string - */ - public function toString(): string - { - return $this->name; - } + /** + * Returns the event name as string + * + * @return string + */ + public function toString(): string + { + return $this->name; + } - /** - * Returns the type of the event (e.g. `page`) - * - * @return string - */ - public function type(): string - { - return $this->type; - } + /** + * Returns the type of the event (e.g. `page`) + * + * @return string + */ + public function type(): string + { + return $this->type; + } - /** - * Updates a given argument with a new value - * - * @internal - * @param string $name - * @param mixed $value - * @return void - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function updateArgument(string $name, $value): void - { - if (array_key_exists($name, $this->arguments) !== true) { - throw new InvalidArgumentException('The argument ' . $name . ' does not exist'); - } + /** + * Updates a given argument with a new value + * + * @internal + * @param string $name + * @param mixed $value + * @return void + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function updateArgument(string $name, $value): void + { + if (array_key_exists($name, $this->arguments) !== true) { + throw new InvalidArgumentException('The argument ' . $name . ' does not exist'); + } - $this->arguments[$name] = $value; - } + $this->arguments[$name] = $value; + } } diff --git a/kirby/src/Cms/Field.php b/kirby/src/Cms/Field.php index 2923f93..d3f97a5 100644 --- a/kirby/src/Cms/Field.php +++ b/kirby/src/Cms/Field.php @@ -26,232 +26,232 @@ use Kirby\Exception\InvalidArgumentException; */ class Field { - /** - * Field method aliases - * - * @var array - */ - public static $aliases = []; + /** + * Field method aliases + * + * @var array + */ + public static $aliases = []; - /** - * The field name - * - * @var string - */ - protected $key; + /** + * The field name + * + * @var string + */ + protected $key; - /** - * Registered field methods - * - * @var array - */ - public static $methods = []; + /** + * Registered field methods + * + * @var array + */ + public static $methods = []; - /** - * The parent object if available. - * This will be the page, site, user or file - * to which the content belongs - * - * @var Model - */ - protected $parent; + /** + * The parent object if available. + * This will be the page, site, user or file + * to which the content belongs + * + * @var Model + */ + protected $parent; - /** - * The value of the field - * - * @var mixed - */ - public $value; + /** + * The value of the field + * + * @var mixed + */ + public $value; - /** - * Magic caller for field methods - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - $method = strtolower($method); + /** + * Magic caller for field methods + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + $method = strtolower($method); - if (isset(static::$methods[$method]) === true) { - return (static::$methods[$method])(clone $this, ...$arguments); - } + if (isset(static::$methods[$method]) === true) { + return (static::$methods[$method])(clone $this, ...$arguments); + } - if (isset(static::$aliases[$method]) === true) { - $method = strtolower(static::$aliases[$method]); + if (isset(static::$aliases[$method]) === true) { + $method = strtolower(static::$aliases[$method]); - if (isset(static::$methods[$method]) === true) { - return (static::$methods[$method])(clone $this, ...$arguments); - } - } + if (isset(static::$methods[$method]) === true) { + return (static::$methods[$method])(clone $this, ...$arguments); + } + } - return $this; - } + return $this; + } - /** - * Creates a new field object - * - * @param object|null $parent - * @param string $key - * @param mixed $value - */ - public function __construct(?object $parent, string $key, $value) - { - $this->key = $key; - $this->value = $value; - $this->parent = $parent; - } + /** + * Creates a new field object + * + * @param object|null $parent + * @param string $key + * @param mixed $value + */ + public function __construct(?object $parent, string $key, $value) + { + $this->key = $key; + $this->value = $value; + $this->parent = $parent; + } - /** - * Simplifies the var_dump result - * - * @see Field::toArray - * @return array - */ - public function __debugInfo() - { - return $this->toArray(); - } + /** + * Simplifies the var_dump result + * + * @see Field::toArray + * @return array + */ + public function __debugInfo() + { + return $this->toArray(); + } - /** - * Makes it possible to simply echo - * or stringify the entire object - * - * @see Field::toString - * @return string - */ - public function __toString(): string - { - return $this->toString(); - } + /** + * Makes it possible to simply echo + * or stringify the entire object + * + * @see Field::toString + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } - /** - * Checks if the field exists in the content data array - * - * @return bool - */ - public function exists(): bool - { - return $this->parent->content()->has($this->key); - } + /** + * Checks if the field exists in the content data array + * + * @return bool + */ + public function exists(): bool + { + return $this->parent->content()->has($this->key); + } - /** - * Checks if the field content is empty - * - * @return bool - */ - public function isEmpty(): bool - { - return empty($this->value) === true && in_array($this->value, [0, '0', false], true) === false; - } + /** + * Checks if the field content is empty + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->value) === true && in_array($this->value, [0, '0', false], true) === false; + } - /** - * Checks if the field content is not empty - * - * @return bool - */ - public function isNotEmpty(): bool - { - return $this->isEmpty() === false; - } + /** + * Checks if the field content is not empty + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->isEmpty() === false; + } - /** - * Returns the name of the field - * - * @return string - */ - public function key(): string - { - return $this->key; - } + /** + * Returns the name of the field + * + * @return string + */ + public function key(): string + { + return $this->key; + } - /** - * @see Field::parent() - * @return \Kirby\Cms\Model|null - */ - public function model() - { - return $this->parent; - } + /** + * @see Field::parent() + * @return \Kirby\Cms\Model|null + */ + public function model() + { + return $this->parent; + } - /** - * Provides a fallback if the field value is empty - * - * @param mixed $fallback - * @return $this|static - */ - public function or($fallback = null) - { - if ($this->isNotEmpty()) { - return $this; - } + /** + * Provides a fallback if the field value is empty + * + * @param mixed $fallback + * @return $this|static + */ + public function or($fallback = null) + { + if ($this->isNotEmpty()) { + return $this; + } - if (is_a($fallback, 'Kirby\Cms\Field') === true) { - return $fallback; - } + if (is_a($fallback, 'Kirby\Cms\Field') === true) { + return $fallback; + } - $field = clone $this; - $field->value = $fallback; - return $field; - } + $field = clone $this; + $field->value = $fallback; + return $field; + } - /** - * Returns the parent object of the field - * - * @return \Kirby\Cms\Model|null - */ - public function parent() - { - return $this->parent; - } + /** + * Returns the parent object of the field + * + * @return \Kirby\Cms\Model|null + */ + public function parent() + { + return $this->parent; + } - /** - * Converts the Field object to an array - * - * @return array - */ - public function toArray(): array - { - return [$this->key => $this->value]; - } + /** + * Converts the Field object to an array + * + * @return array + */ + public function toArray(): array + { + return [$this->key => $this->value]; + } - /** - * Returns the field value as string - * - * @return string - */ - public function toString(): string - { - return (string)$this->value; - } + /** + * Returns the field value as string + * + * @return string + */ + public function toString(): string + { + return (string)$this->value; + } - /** - * Returns the field content. If a new value is passed, - * the modified field will be returned. Otherwise it - * will return the field value. - * - * @param string|\Closure $value - * @return mixed - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function value($value = null) - { - if ($value === null) { - return $this->value; - } + /** + * Returns the field content. If a new value is passed, + * the modified field will be returned. Otherwise it + * will return the field value. + * + * @param string|\Closure $value + * @return mixed + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function value($value = null) + { + if ($value === null) { + return $this->value; + } - if (is_scalar($value)) { - $value = (string)$value; - } elseif (is_callable($value)) { - $value = (string)$value->call($this, $this->value); - } else { - throw new InvalidArgumentException('Invalid field value type: ' . gettype($value)); - } + if (is_scalar($value)) { + $value = (string)$value; + } elseif (is_callable($value)) { + $value = (string)$value->call($this, $this->value); + } else { + throw new InvalidArgumentException('Invalid field value type: ' . gettype($value)); + } - $clone = clone $this; - $clone->value = $value; + $clone = clone $this; + $clone->value = $value; - return $clone; - } + return $clone; + } } diff --git a/kirby/src/Cms/Fieldset.php b/kirby/src/Cms/Fieldset.php index eb6e94b..0b49fd4 100644 --- a/kirby/src/Cms/Fieldset.php +++ b/kirby/src/Cms/Fieldset.php @@ -19,276 +19,276 @@ use Kirby\Toolkit\Str; */ class Fieldset extends Item { - public const ITEMS_CLASS = '\Kirby\Cms\Fieldsets'; + public const ITEMS_CLASS = '\Kirby\Cms\Fieldsets'; - protected $disabled; - protected $editable; - protected $fields = []; - protected $icon; - protected $label; - protected $model; - protected $name; - protected $preview; - protected $tabs; - protected $translate; - protected $type; - protected $unset; - protected $wysiwyg; + protected $disabled; + protected $editable; + protected $fields = []; + protected $icon; + protected $label; + protected $model; + protected $name; + protected $preview; + protected $tabs; + protected $translate; + protected $type; + protected $unset; + protected $wysiwyg; - /** - * Creates a new Fieldset object - * - * @param array $params - */ - public function __construct(array $params = []) - { - if (empty($params['type']) === true) { - throw new InvalidArgumentException('The fieldset type is missing'); - } + /** + * Creates a new Fieldset object + * + * @param array $params + */ + public function __construct(array $params = []) + { + if (empty($params['type']) === true) { + throw new InvalidArgumentException('The fieldset type is missing'); + } - $this->type = $params['id'] = $params['type']; + $this->type = $params['id'] = $params['type']; - parent::__construct($params); + parent::__construct($params); - $this->disabled = $params['disabled'] ?? false; - $this->editable = $params['editable'] ?? true; - $this->icon = $params['icon'] ?? null; - $this->model = $this->parent; - $this->name = $this->createName($params['name'] ?? Str::ucfirst($this->type)); - $this->label = $this->createLabel($params['label'] ?? null); - $this->preview = $params['preview'] ?? null; - $this->tabs = $this->createTabs($params); - $this->translate = $params['translate'] ?? true; - $this->unset = $params['unset'] ?? false; - $this->wysiwyg = $params['wysiwyg'] ?? false; + $this->disabled = $params['disabled'] ?? false; + $this->editable = $params['editable'] ?? true; + $this->icon = $params['icon'] ?? null; + $this->model = $this->parent; + $this->name = $this->createName($params['title'] ?? $params['name'] ?? Str::ucfirst($this->type)); + $this->label = $this->createLabel($params['label'] ?? null); + $this->preview = $params['preview'] ?? null; + $this->tabs = $this->createTabs($params); + $this->translate = $params['translate'] ?? true; + $this->unset = $params['unset'] ?? false; + $this->wysiwyg = $params['wysiwyg'] ?? false; - if ( - $this->translate === false && - $this->kirby()->multilang() === true && - $this->kirby()->language()->isDefault() === false - ) { - // disable and unset the fieldset if it's not translatable - $this->unset = true; - $this->disabled = true; - } - } + if ( + $this->translate === false && + $this->kirby()->multilang() === true && + $this->kirby()->language()->isDefault() === false + ) { + // disable and unset the fieldset if it's not translatable + $this->unset = true; + $this->disabled = true; + } + } - /** - * @param array $fields - * @return array - */ - protected function createFields(array $fields = []): array - { - $fields = Blueprint::fieldsProps($fields); - $fields = $this->form($fields)->fields()->toArray(); + /** + * @param array $fields + * @return array + */ + protected function createFields(array $fields = []): array + { + $fields = Blueprint::fieldsProps($fields); + $fields = $this->form($fields)->fields()->toArray(); - // collect all fields - $this->fields = array_merge($this->fields, $fields); + // collect all fields + $this->fields = array_merge($this->fields, $fields); - return $fields; - } + return $fields; + } - /** - * @param array|string $name - * @return string|null - */ - protected function createName($name): ?string - { - return I18n::translate($name, $name); - } + /** + * @param array|string $name + * @return string|null + */ + protected function createName($name): ?string + { + return I18n::translate($name, $name); + } - /** - * @param array|string $label - * @return string|null - */ - protected function createLabel($label = null): ?string - { - return I18n::translate($label, $label); - } + /** + * @param array|string $label + * @return string|null + */ + protected function createLabel($label = null): ?string + { + return I18n::translate($label, $label); + } - /** - * @param array $params - * @return array - */ - protected function createTabs(array $params = []): array - { - $tabs = $params['tabs'] ?? []; + /** + * @param array $params + * @return array + */ + protected function createTabs(array $params = []): array + { + $tabs = $params['tabs'] ?? []; - // return a single tab if there are only fields - if (empty($tabs) === true) { - return [ - 'content' => [ - 'fields' => $this->createFields($params['fields'] ?? []), - ] - ]; - } + // return a single tab if there are only fields + if (empty($tabs) === true) { + return [ + 'content' => [ + 'fields' => $this->createFields($params['fields'] ?? []), + ] + ]; + } - // normalize tabs props - foreach ($tabs as $name => $tab) { - // unset/remove tab if its property is false - if ($tab === false) { - unset($tabs[$name]); - continue; - } + // normalize tabs props + foreach ($tabs as $name => $tab) { + // unset/remove tab if its property is false + if ($tab === false) { + unset($tabs[$name]); + continue; + } - $tab = Blueprint::extend($tab); + $tab = Blueprint::extend($tab); - $tab['fields'] = $this->createFields($tab['fields'] ?? []); - $tab['label'] = $this->createLabel($tab['label'] ?? Str::ucfirst($name)); - $tab['name'] = $name; + $tab['fields'] = $this->createFields($tab['fields'] ?? []); + $tab['label'] = $this->createLabel($tab['label'] ?? Str::ucfirst($name)); + $tab['name'] = $name; - $tabs[$name] = $tab; - } + $tabs[$name] = $tab; + } - return $tabs; - } + return $tabs; + } - /** - * @return bool - */ - public function disabled(): bool - { - return $this->disabled; - } + /** + * @return bool + */ + public function disabled(): bool + { + return $this->disabled; + } - /** - * @return bool - */ - public function editable(): bool - { - if ($this->editable === false) { - return false; - } + /** + * @return bool + */ + public function editable(): bool + { + if ($this->editable === false) { + return false; + } - if (count($this->fields) === 0) { - return false; - } + if (count($this->fields) === 0) { + return false; + } - return true; - } + return true; + } - /** - * @return array - */ - public function fields(): array - { - return $this->fields; - } + /** + * @return array + */ + public function fields(): array + { + return $this->fields; + } - /** - * Creates a form for the given fields - * - * @param array $fields - * @param array $input - * @return \Kirby\Form\Form - */ - public function form(array $fields, array $input = []) - { - return new Form([ - 'fields' => $fields, - 'model' => $this->model, - 'strict' => true, - 'values' => $input, - ]); - } + /** + * Creates a form for the given fields + * + * @param array $fields + * @param array $input + * @return \Kirby\Form\Form + */ + public function form(array $fields, array $input = []) + { + return new Form([ + 'fields' => $fields, + 'model' => $this->model, + 'strict' => true, + 'values' => $input, + ]); + } - /** - * @return string|null - */ - public function icon(): ?string - { - return $this->icon; - } + /** + * @return string|null + */ + public function icon(): ?string + { + return $this->icon; + } - /** - * @return string|null - */ - public function label(): ?string - { - return $this->label; - } + /** + * @return string|null + */ + public function label(): ?string + { + return $this->label; + } - /** - * @return \Kirby\Cms\ModelWithContent - */ - public function model() - { - return $this->model; - } + /** + * @return \Kirby\Cms\ModelWithContent + */ + public function model() + { + return $this->model; + } - /** - * @return string - */ - public function name(): string - { - return $this->name; - } + /** + * @return string + */ + public function name(): string + { + return $this->name; + } - /** - * @return string|bool - */ - public function preview() - { - return $this->preview; - } + /** + * @return string|bool + */ + public function preview() + { + return $this->preview; + } - /** - * @return array - */ - public function tabs(): array - { - return $this->tabs; - } + /** + * @return array + */ + public function tabs(): array + { + return $this->tabs; + } - /** - * @return bool - */ - public function translate(): bool - { - return $this->translate; - } + /** + * @return bool + */ + public function translate(): bool + { + return $this->translate; + } - /** - * @return string - */ - public function type(): string - { - return $this->type; - } + /** + * @return string + */ + public function type(): string + { + return $this->type; + } - /** - * @return array - */ - public function toArray(): array - { - return [ - 'disabled' => $this->disabled(), - 'editable' => $this->editable(), - 'icon' => $this->icon(), - 'label' => $this->label(), - 'name' => $this->name(), - 'preview' => $this->preview(), - 'tabs' => $this->tabs(), - 'translate' => $this->translate(), - 'type' => $this->type(), - 'unset' => $this->unset(), - 'wysiwyg' => $this->wysiwyg(), - ]; - } + /** + * @return array + */ + public function toArray(): array + { + return [ + 'disabled' => $this->disabled(), + 'editable' => $this->editable(), + 'icon' => $this->icon(), + 'label' => $this->label(), + 'name' => $this->name(), + 'preview' => $this->preview(), + 'tabs' => $this->tabs(), + 'translate' => $this->translate(), + 'type' => $this->type(), + 'unset' => $this->unset(), + 'wysiwyg' => $this->wysiwyg(), + ]; + } - /** - * @return bool - */ - public function unset(): bool - { - return $this->unset; - } + /** + * @return bool + */ + public function unset(): bool + { + return $this->unset; + } - /** - * @return bool - */ - public function wysiwyg(): bool - { - return $this->wysiwyg; - } + /** + * @return bool + */ + public function wysiwyg(): bool + { + return $this->wysiwyg; + } } diff --git a/kirby/src/Cms/Fieldsets.php b/kirby/src/Cms/Fieldsets.php index 29d3177..6189b22 100644 --- a/kirby/src/Cms/Fieldsets.php +++ b/kirby/src/Cms/Fieldsets.php @@ -19,85 +19,85 @@ use Kirby\Toolkit\Str; */ class Fieldsets extends Items { - public const ITEM_CLASS = '\Kirby\Cms\Fieldset'; + public const ITEM_CLASS = '\Kirby\Cms\Fieldset'; - protected static function createFieldsets($params) - { - $fieldsets = []; - $groups = []; + protected static function createFieldsets($params) + { + $fieldsets = []; + $groups = []; - foreach ($params as $type => $fieldset) { - if (is_int($type) === true && is_string($fieldset)) { - $type = $fieldset; - $fieldset = 'blocks/' . $type; - } + foreach ($params as $type => $fieldset) { + if (is_int($type) === true && is_string($fieldset)) { + $type = $fieldset; + $fieldset = 'blocks/' . $type; + } - if ($fieldset === false) { - continue; - } + if ($fieldset === false) { + continue; + } - if ($fieldset === true) { - $fieldset = 'blocks/' . $type; - } + if ($fieldset === true) { + $fieldset = 'blocks/' . $type; + } - $fieldset = Blueprint::extend($fieldset); + $fieldset = Blueprint::extend($fieldset); - // make sure the type is always set - $fieldset['type'] ??= $type; + // make sure the type is always set + $fieldset['type'] ??= $type; - // extract groups - if ($fieldset['type'] === 'group') { - $result = static::createFieldsets($fieldset['fieldsets'] ?? []); - $fieldsets = array_merge($fieldsets, $result['fieldsets']); - $label = $fieldset['label'] ?? Str::ucfirst($type); + // extract groups + if ($fieldset['type'] === 'group') { + $result = static::createFieldsets($fieldset['fieldsets'] ?? []); + $fieldsets = array_merge($fieldsets, $result['fieldsets']); + $label = $fieldset['label'] ?? Str::ucfirst($type); - $groups[$type] = [ - 'label' => I18n::translate($label, $label), - 'name' => $type, - 'open' => $fieldset['open'] ?? true, - 'sets' => array_column($result['fieldsets'], 'type'), - ]; - } else { - $fieldsets[$fieldset['type']] = $fieldset; - } - } + $groups[$type] = [ + 'label' => I18n::translate($label, $label), + 'name' => $type, + 'open' => $fieldset['open'] ?? true, + 'sets' => array_column($result['fieldsets'], 'type'), + ]; + } else { + $fieldsets[$fieldset['type']] = $fieldset; + } + } - return [ - 'fieldsets' => $fieldsets, - 'groups' => $groups - ]; - } + return [ + 'fieldsets' => $fieldsets, + 'groups' => $groups + ]; + } - public static function factory(array $items = null, array $params = []) - { - $items ??= option('blocks.fieldsets', [ - 'code' => 'blocks/code', - 'gallery' => 'blocks/gallery', - 'heading' => 'blocks/heading', - 'image' => 'blocks/image', - 'line' => 'blocks/line', - 'list' => 'blocks/list', - 'markdown' => 'blocks/markdown', - 'quote' => 'blocks/quote', - 'text' => 'blocks/text', - 'video' => 'blocks/video', - ]); + public static function factory(array $items = null, array $params = []) + { + $items ??= App::instance()->option('blocks.fieldsets', [ + 'code' => 'blocks/code', + 'gallery' => 'blocks/gallery', + 'heading' => 'blocks/heading', + 'image' => 'blocks/image', + 'line' => 'blocks/line', + 'list' => 'blocks/list', + 'markdown' => 'blocks/markdown', + 'quote' => 'blocks/quote', + 'text' => 'blocks/text', + 'video' => 'blocks/video', + ]); - $result = static::createFieldsets($items); + $result = static::createFieldsets($items); - return parent::factory($result['fieldsets'], ['groups' => $result['groups']] + $params); - } + return parent::factory($result['fieldsets'], ['groups' => $result['groups']] + $params); + } - public function groups(): array - { - return $this->options['groups'] ?? []; - } + public function groups(): array + { + return $this->options['groups'] ?? []; + } - public function toArray(?Closure $map = null): array - { - return A::map( - $this->data, - $map ?? fn ($fieldset) => $fieldset->toArray() - ); - } + public function toArray(?Closure $map = null): array + { + return A::map( + $this->data, + $map ?? fn ($fieldset) => $fieldset->toArray() + ); + } } diff --git a/kirby/src/Cms/File.php b/kirby/src/Cms/File.php index 026dc65..bc81c7d 100644 --- a/kirby/src/Cms/File.php +++ b/kirby/src/Cms/File.php @@ -30,733 +30,724 @@ use Kirby\Toolkit\Str; */ class File extends ModelWithContent { - use FileActions; - use FileModifications; - use HasMethods; - use HasSiblings; - use IsFile; - - public const CLASS_ALIAS = 'file'; - - /** - * Cache for the initialized blueprint object - * - * @var \Kirby\Cms\FileBlueprint - */ - protected $blueprint; - - /** - * @var string - */ - protected $filename; - - /** - * @var string - */ - protected $id; - - /** - * All registered file methods - * - * @var array - */ - public static $methods = []; - - /** - * The parent object - * - * @var \Kirby\Cms\Model - */ - protected $parent; - - /** - * The absolute path to the file - * - * @var string|null - */ - protected $root; - - /** - * @var string - */ - protected $template; - - /** - * The public file Url - * - * @var string - */ - protected $url; - - /** - * Magic caller for file methods - * and content fields. (in this order) - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } - - // asset method proxy - if (method_exists($this->asset(), $method)) { - return $this->asset()->$method(...$arguments); - } - - // file methods - if ($this->hasMethod($method)) { - return $this->callMethod($method, $arguments); - } - - // content fields - return $this->content()->get($method); - } - - /** - * Creates a new File object - * - * @param array $props - */ - public function __construct(array $props) - { - // set filename as the most important prop first - // TODO: refactor later to avoid redundant prop setting - $this->setProperty('filename', $props['filename'] ?? null, true); - - // set other properties - $this->setProperties($props); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return array_merge($this->toArray(), [ - 'content' => $this->content(), - 'siblings' => $this->siblings(), - ]); - } - - /** - * Returns the url to api endpoint - * - * @internal - * @param bool $relative - * @return string - */ - public function apiUrl(bool $relative = false): string - { - return $this->parent()->apiUrl($relative) . '/files/' . $this->filename(); - } - - /** - * Returns the FileBlueprint object for the file - * - * @return \Kirby\Cms\FileBlueprint - */ - public function blueprint() - { - if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) { - return $this->blueprint; - } - - return $this->blueprint = FileBlueprint::factory('files/' . $this->template(), 'files/default', $this); - } - - /** - * Store the template in addition to the - * other content. - * - * @internal - * @param array $data - * @param string|null $languageCode - * @return array - */ - public function contentFileData(array $data, string $languageCode = null): array - { - return A::append($data, [ - 'template' => $this->template(), - ]); - } - - /** - * Returns the directory in which - * the content file is located - * - * @internal - * @return string - */ - public function contentFileDirectory(): string - { - return dirname($this->root()); - } - - /** - * Filename for the content file - * - * @internal - * @return string - */ - public function contentFileName(): string - { - return $this->filename(); - } - - /** - * Constructs a File object - * - * @internal - * @param mixed $props - * @return static - */ - public static function factory($props) - { - return new static($props); - } - - /** - * Returns the filename with extension - * - * @return string - */ - public function filename(): string - { - return $this->filename; - } - - /** - * Returns the parent Files collection - * - * @return \Kirby\Cms\Files - */ - public function files() - { - return $this->siblingsCollection(); - } - - /** - * Converts the file to html - * - * @param array $attr - * @return string - */ - public function html(array $attr = []): string - { - return $this->asset()->html(array_merge( - ['alt' => $this->alt()], - $attr - )); - } - - /** - * Returns the id - * - * @return string - */ - public function id(): string - { - if ($this->id !== null) { - return $this->id; - } - - if (is_a($this->parent(), 'Kirby\Cms\Page') === true) { - return $this->id = $this->parent()->id() . '/' . $this->filename(); - } elseif (is_a($this->parent(), 'Kirby\Cms\User') === true) { - return $this->id = $this->parent()->id() . '/' . $this->filename(); - } - - return $this->id = $this->filename(); - } - - /** - * Compares the current object with the given file object - * - * @param \Kirby\Cms\File $file - * @return bool - */ - public function is(File $file): bool - { - return $this->id() === $file->id(); - } - - /** - * Check if the file can be read by the current user - * - * @return bool - */ - public function isReadable(): bool - { - static $readable = []; - - $template = $this->template(); - - if (isset($readable[$template]) === true) { - return $readable[$template]; - } - - return $readable[$template] = $this->permissions()->can('read'); - } - - /** - * Creates a unique media hash - * - * @internal - * @return string - */ - public function mediaHash(): string - { - return $this->mediaToken() . '-' . $this->modifiedFile(); - } - - /** - * Returns the absolute path to the file in the public media folder - * - * @internal - * @return string - */ - public function mediaRoot(): string - { - return $this->parent()->mediaRoot() . '/' . $this->mediaHash() . '/' . $this->filename(); - } - - /** - * Creates a non-guessable token string for this file - * - * @internal - * @return string - */ - public function mediaToken(): string - { - $token = $this->kirby()->contentToken($this, $this->id()); - return substr($token, 0, 10); - } - - /** - * Returns the absolute Url to the file in the public media folder - * - * @internal - * @return string - */ - public function mediaUrl(): string - { - return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename(); - } - - /** - * Get the file's last modification time. - * - * @param string|null $format - * @param string|null $handler date or strftime - * @param string|null $languageCode - * @return mixed - */ - public function modified(string $format = null, string $handler = null, string $languageCode = null) - { - $file = $this->modifiedFile(); - $content = $this->modifiedContent($languageCode); - $modified = max($file, $content); - $handler ??= $this->kirby()->option('date.handler', 'date'); - - return Str::date($modified, $format, $handler); - } - - /** - * Timestamp of the last modification - * of the content file - * - * @param string|null $languageCode - * @return int - */ - protected function modifiedContent(string $languageCode = null): int - { - return F::modified($this->contentFile($languageCode)); - } - - /** - * Timestamp of the last modification - * of the source file - * - * @return int - */ - protected function modifiedFile(): int - { - return F::modified($this->root()); - } - - /** - * Returns the parent Page object - * - * @return \Kirby\Cms\Page|null - */ - public function page() - { - return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null; - } - - /** - * Returns the panel info object - * - * @return \Kirby\Panel\File - */ - public function panel() - { - return new Panel($this); - } - - /** - * Returns the parent Model object - * - * @return \Kirby\Cms\Model - */ - public function parent() - { - return $this->parent ??= $this->kirby()->site(); - } - - /** - * Returns the parent id if a parent exists - * - * @internal - * @todo 3.7.0 When setParent() is changed, the if check is not needed anymore - * @return string|null - */ - public function parentId(): ?string - { - if ($parent = $this->parent()) { - return $parent->id(); - } - - return null; - } - - /** - * Returns a collection of all parent pages - * - * @return \Kirby\Cms\Pages - */ - public function parents() - { - if (is_a($this->parent(), 'Kirby\Cms\Page') === true) { - return $this->parent()->parents()->prepend($this->parent()->id(), $this->parent()); - } - - return new Pages(); - } - - /** - * Returns the permissions object for this file - * - * @return \Kirby\Cms\FilePermissions - */ - public function permissions() - { - return new FilePermissions($this); - } - - /** - * Returns the absolute root to the file - * - * @return string|null - */ - public function root(): ?string - { - return $this->root ??= $this->parent()->root() . '/' . $this->filename(); - } - - /** - * Returns the FileRules class to - * validate any important action. - * - * @return \Kirby\Cms\FileRules - */ - protected function rules() - { - return new FileRules(); - } - - /** - * Sets the Blueprint object - * - * @param array|null $blueprint - * @return $this - */ - protected function setBlueprint(array $blueprint = null) - { - if ($blueprint !== null) { - $blueprint['model'] = $this; - $this->blueprint = new FileBlueprint($blueprint); - } - - return $this; - } - - /** - * Sets the filename - * - * @param string $filename - * @return $this - */ - protected function setFilename(string $filename) - { - $this->filename = $filename; - return $this; - } - - /** - * Sets the parent model object; - * this property is required for `File::create()` and - * will be generally required starting with Kirby 3.7.0 - * - * @param \Kirby\Cms\Model|null $parent - * @return $this - * @todo make property required in 3.7.0 - */ - protected function setParent(Model $parent = null) - { - // @codeCoverageIgnoreStart - if ($parent === null) { - deprecated('You are creating a `Kirby\Cms\File` object without passing the `parent` property. While unsupported, this hasn\'t caused any direct errors so far. To fix inconsistencies, the `parent` property will be required when creating a `Kirby\Cms\File` object in Kirby 3.7.0 and higher. Not passing this property will start throwing a breaking error.'); - } - // @codeCoverageIgnoreEnd - - $this->parent = $parent; - return $this; - } - - /** - * Always set the root to null, to invoke - * auto root detection - * - * @param string|null $root - * @return $this - */ - protected function setRoot(string $root = null) - { - $this->root = null; - return $this; - } - - /** - * @param string|null $template - * @return $this - */ - protected function setTemplate(string $template = null) - { - $this->template = $template; - return $this; - } - - /** - * Sets the url - * - * @param string|null $url - * @return $this - */ - protected function setUrl(string $url = null) - { - $this->url = $url; - return $this; - } - - /** - * Returns the parent Files collection - * @internal - * - * @return \Kirby\Cms\Files - */ - protected function siblingsCollection() - { - return $this->parent()->files(); - } - - /** - * Returns the parent Site object - * - * @return \Kirby\Cms\Site - */ - public function site() - { - return is_a($this->parent(), 'Kirby\Cms\Site') === true ? $this->parent() : $this->kirby()->site(); - } - - /** - * Returns the final template - * - * @return string|null - */ - public function template(): ?string - { - return $this->template ??= $this->content()->get('template')->value(); - } - - /** - * Returns siblings with the same template - * - * @param bool $self - * @return \Kirby\Cms\Files - */ - public function templateSiblings(bool $self = true) - { - return $this->siblings($self)->filter('template', $this->template()); - } - - /** - * Extended info for the array export - * by injecting the information from - * the asset. - * - * @return array - */ - public function toArray(): array - { - return array_merge($this->asset()->toArray(), parent::toArray()); - } - - /** - * Returns the Url - * - * @return string - */ - public function url(): string - { - return $this->url ??= ($this->kirby()->component('file::url'))($this->kirby(), $this); - } - - - /** - * Deprecated! - */ - - /** - * Provides a kirbytag or markdown - * tag for the file, which will be - * used in the panel, when the file - * gets dragged onto a textarea - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param string|null $type (null|auto|kirbytext|markdown) - * @param bool $absolute - * @return string - * @codeCoverageIgnore - */ - public function dragText(string $type = null, bool $absolute = false): string - { - return $this->panel()->dragText($type, $absolute); - } - - /** - * Returns an array of all actions - * that can be performed in the Panel - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @since 3.3.0 This also checks for the lock status - * @since 3.5.1 This also checks for matching accept settings - * - * @param array $unlock An array of options that will be force-unlocked - * @return array - * @codeCoverageIgnore - */ - public function panelOptions(array $unlock = []): array - { - return $this->panel()->options($unlock); - } - - /** - * Returns the full path without leading slash - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @return string - * @codeCoverageIgnore - */ - public function panelPath(): string - { - return $this->panel()->path(); - } - - /** - * Prepares the response data for file pickers - * and file fields - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @param array|null $params - * @return array - * @codeCoverageIgnore - */ - public function panelPickerData(array $params = []): array - { - return $this->panel()->pickerData($params); - } - - /** - * Returns the url to the editing view - * in the panel - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param bool $relative - * @return string - * @codeCoverageIgnore - */ - public function panelUrl(bool $relative = false): string - { - return $this->panel()->url($relative); - } - - /** - * Simplified File URL that uses the parent - * Page URL and the filename as a more stable - * alternative for the media URLs. - * - * @return string - */ - public function previewUrl(): string - { - $parent = $this->parent(); - $url = url($this->id()); - - switch ($parent::CLASS_ALIAS) { - case 'page': - $preview = $parent->blueprint()->preview(); - - // the page has a custom preview setting, - // thus the file is only accessible through - // the direct media URL - if ($preview !== true) { - return $this->url(); - } - - // it's more stable to access files for drafts - // through their direct URL to avoid conflicts - // with draft token verification - if ($parent->isDraft() === true) { - return $this->url(); - } - - return $url; - case 'user': - return $this->url(); - default: - return $url; - } - } + use FileActions; + use FileModifications; + use HasMethods; + use HasSiblings; + use IsFile; + + public const CLASS_ALIAS = 'file'; + + /** + * Cache for the initialized blueprint object + * + * @var \Kirby\Cms\FileBlueprint + */ + protected $blueprint; + + /** + * @var string + */ + protected $filename; + + /** + * @var string + */ + protected $id; + + /** + * All registered file methods + * + * @var array + */ + public static $methods = []; + + /** + * The parent object + * + * @var \Kirby\Cms\Model + */ + protected $parent; + + /** + * The absolute path to the file + * + * @var string|null + */ + protected $root; + + /** + * @var string + */ + protected $template; + + /** + * The public file Url + * + * @var string + */ + protected $url; + + /** + * Magic caller for file methods + * and content fields. (in this order) + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // asset method proxy + if (method_exists($this->asset(), $method)) { + return $this->asset()->$method(...$arguments); + } + + // file methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // content fields + return $this->content()->get($method); + } + + /** + * Creates a new File object + * + * @param array $props + */ + public function __construct(array $props) + { + // set filename as the most important prop first + // TODO: refactor later to avoid redundant prop setting + $this->setProperty('filename', $props['filename'] ?? null, true); + + // set other properties + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'content' => $this->content(), + 'siblings' => $this->siblings(), + ]); + } + + /** + * Returns the url to api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + return $this->parent()->apiUrl($relative) . '/files/' . $this->filename(); + } + + /** + * Returns the FileBlueprint object for the file + * + * @return \Kirby\Cms\FileBlueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\FileBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = FileBlueprint::factory('files/' . $this->template(), 'files/default', $this); + } + + /** + * Store the template in addition to the + * other content. + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return A::append($data, [ + 'template' => $this->template(), + ]); + } + + /** + * Returns the directory in which + * the content file is located + * + * @internal + * @return string + */ + public function contentFileDirectory(): string + { + return dirname($this->root()); + } + + /** + * Filename for the content file + * + * @internal + * @return string + */ + public function contentFileName(): string + { + return $this->filename(); + } + + /** + * Constructs a File object + * + * @internal + * @param mixed $props + * @return static + */ + public static function factory($props) + { + return new static($props); + } + + /** + * Returns the filename with extension + * + * @return string + */ + public function filename(): string + { + return $this->filename; + } + + /** + * Returns the parent Files collection + * + * @return \Kirby\Cms\Files + */ + public function files() + { + return $this->siblingsCollection(); + } + + /** + * Converts the file to html + * + * @param array $attr + * @return string + */ + public function html(array $attr = []): string + { + return $this->asset()->html(array_merge( + ['alt' => $this->alt()], + $attr + )); + } + + /** + * Returns the id + * + * @return string + */ + public function id(): string + { + if ($this->id !== null) { + return $this->id; + } + + if (is_a($this->parent(), 'Kirby\Cms\Page') === true) { + return $this->id = $this->parent()->id() . '/' . $this->filename(); + } elseif (is_a($this->parent(), 'Kirby\Cms\User') === true) { + return $this->id = $this->parent()->id() . '/' . $this->filename(); + } + + return $this->id = $this->filename(); + } + + /** + * Compares the current object with the given file object + * + * @param \Kirby\Cms\File $file + * @return bool + */ + public function is(File $file): bool + { + return $this->id() === $file->id(); + } + + /** + * Check if the file can be read by the current user + * + * @return bool + */ + public function isReadable(): bool + { + static $readable = []; + + $template = $this->template(); + + if (isset($readable[$template]) === true) { + return $readable[$template]; + } + + return $readable[$template] = $this->permissions()->can('read'); + } + + /** + * Creates a unique media hash + * + * @internal + * @return string + */ + public function mediaHash(): string + { + return $this->mediaToken() . '-' . $this->modifiedFile(); + } + + /** + * Returns the absolute path to the file in the public media folder + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->parent()->mediaRoot() . '/' . $this->mediaHash() . '/' . $this->filename(); + } + + /** + * Creates a non-guessable token string for this file + * + * @internal + * @return string + */ + public function mediaToken(): string + { + $token = $this->kirby()->contentToken($this, $this->id()); + return substr($token, 0, 10); + } + + /** + * Returns the absolute Url to the file in the public media folder + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->parent()->mediaUrl() . '/' . $this->mediaHash() . '/' . $this->filename(); + } + + /** + * Get the file's last modification time. + * + * @param string|\IntlDateFormatter|null $format + * @param string|null $handler date, intl or strftime + * @param string|null $languageCode + * @return mixed + */ + public function modified($format = null, string $handler = null, string $languageCode = null) + { + $file = $this->modifiedFile(); + $content = $this->modifiedContent($languageCode); + $modified = max($file, $content); + $handler ??= $this->kirby()->option('date.handler', 'date'); + + return Str::date($modified, $format, $handler); + } + + /** + * Timestamp of the last modification + * of the content file + * + * @param string|null $languageCode + * @return int + */ + protected function modifiedContent(string $languageCode = null): int + { + return F::modified($this->contentFile($languageCode)); + } + + /** + * Timestamp of the last modification + * of the source file + * + * @return int + */ + protected function modifiedFile(): int + { + return F::modified($this->root()); + } + + /** + * Returns the parent Page object + * + * @return \Kirby\Cms\Page|null + */ + public function page() + { + return is_a($this->parent(), 'Kirby\Cms\Page') === true ? $this->parent() : null; + } + + /** + * Returns the panel info object + * + * @return \Kirby\Panel\File + */ + public function panel() + { + return new Panel($this); + } + + /** + * Returns the parent Model object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent ??= $this->kirby()->site(); + } + + /** + * Returns the parent id if a parent exists + * + * @internal + * @return string + */ + public function parentId(): string + { + return $this->parent()->id(); + } + + /** + * Returns a collection of all parent pages + * + * @return \Kirby\Cms\Pages + */ + public function parents() + { + if (is_a($this->parent(), 'Kirby\Cms\Page') === true) { + return $this->parent()->parents()->prepend($this->parent()->id(), $this->parent()); + } + + return new Pages(); + } + + /** + * Returns the permissions object for this file + * + * @return \Kirby\Cms\FilePermissions + */ + public function permissions() + { + return new FilePermissions($this); + } + + /** + * Returns the absolute root to the file + * + * @return string|null + */ + public function root(): ?string + { + return $this->root ??= $this->parent()->root() . '/' . $this->filename(); + } + + /** + * Returns the FileRules class to + * validate any important action. + * + * @return \Kirby\Cms\FileRules + */ + protected function rules() + { + return new FileRules(); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return $this + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new FileBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the filename + * + * @param string $filename + * @return $this + */ + protected function setFilename(string $filename) + { + $this->filename = $filename; + return $this; + } + + /** + * Sets the parent model object + * + * @param \Kirby\Cms\Model $parent + * @return $this + */ + protected function setParent(Model $parent) + { + $this->parent = $parent; + return $this; + } + + /** + * Always set the root to null, to invoke + * auto root detection + * + * @param string|null $root + * @return $this + */ + protected function setRoot(string $root = null) + { + $this->root = null; + return $this; + } + + /** + * @param string|null $template + * @return $this + */ + protected function setTemplate(string $template = null) + { + $this->template = $template; + return $this; + } + + /** + * Sets the url + * + * @param string|null $url + * @return $this + */ + protected function setUrl(string $url = null) + { + $this->url = $url; + return $this; + } + + /** + * Returns the parent Files collection + * @internal + * + * @return \Kirby\Cms\Files + */ + protected function siblingsCollection() + { + return $this->parent()->files(); + } + + /** + * Returns the parent Site object + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return is_a($this->parent(), 'Kirby\Cms\Site') === true ? $this->parent() : $this->kirby()->site(); + } + + /** + * Returns the final template + * + * @return string|null + */ + public function template(): ?string + { + return $this->template ??= $this->content()->get('template')->value(); + } + + /** + * Returns siblings with the same template + * + * @param bool $self + * @return \Kirby\Cms\Files + */ + public function templateSiblings(bool $self = true) + { + return $this->siblings($self)->filter('template', $this->template()); + } + + /** + * Extended info for the array export + * by injecting the information from + * the asset. + * + * @return array + */ + public function toArray(): array + { + return array_merge($this->asset()->toArray(), parent::toArray()); + } + + /** + * Returns the Url + * + * @return string + */ + public function url(): string + { + return $this->url ??= ($this->kirby()->component('file::url'))($this->kirby(), $this); + } + + + /** + * Deprecated! + */ + + /** + * Provides a kirbytag or markdown + * tag for the file, which will be + * used in the panel, when the file + * gets dragged onto a textarea + * + * @todo Remove in 3.8.0 + * + * @internal + * @param string|null $type (null|auto|kirbytext|markdown) + * @param bool $absolute + * @return string + * @codeCoverageIgnore + */ + public function dragText(string $type = null, bool $absolute = false): string + { + Helpers::deprecated('Cms\File::dragText() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->dragText() instead.'); + return $this->panel()->dragText($type, $absolute); + } + + /** + * Returns an array of all actions + * that can be performed in the Panel + * + * @todo Remove in 3.8.0 + * + * @since 3.3.0 This also checks for the lock status + * @since 3.5.1 This also checks for matching accept settings + * + * @param array $unlock An array of options that will be force-unlocked + * @return array + * @codeCoverageIgnore + */ + public function panelOptions(array $unlock = []): array + { + Helpers::deprecated('Cms\File::panelOptions() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->options() instead.'); + return $this->panel()->options($unlock); + } + + /** + * Returns the full path without leading slash + * + * @todo Remove in 3.8.0 + * + * @internal + * @return string + * @codeCoverageIgnore + */ + public function panelPath(): string + { + Helpers::deprecated('Cms\File::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->path() instead.'); + return $this->panel()->path(); + } + + /** + * Prepares the response data for file pickers + * and file fields + * + * @todo Remove in 3.8.0 + * + * @param array|null $params + * @return array + * @codeCoverageIgnore + */ + public function panelPickerData(array $params = []): array + { + Helpers::deprecated('Cms\File::panelPickerData() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->pickerData() instead.'); + return $this->panel()->pickerData($params); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @todo Remove in 3.8.0 + * + * @internal + * @param bool $relative + * @return string + * @codeCoverageIgnore + */ + public function panelUrl(bool $relative = false): string + { + Helpers::deprecated('Cms\File::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $file->panel()->url() instead.'); + return $this->panel()->url($relative); + } + + /** + * Simplified File URL that uses the parent + * Page URL and the filename as a more stable + * alternative for the media URLs. + * + * @return string + */ + public function previewUrl(): string + { + $parent = $this->parent(); + $url = Url::to($this->id()); + + switch ($parent::CLASS_ALIAS) { + case 'page': + $preview = $parent->blueprint()->preview(); + + // the page has a custom preview setting, + // thus the file is only accessible through + // the direct media URL + if ($preview !== true) { + return $this->url(); + } + + // it's more stable to access files for drafts + // through their direct URL to avoid conflicts + // with draft token verification + if ($parent->isDraft() === true) { + return $this->url(); + } + + // checks `file::url` component is extended + if ($this->kirby()->isNativeComponent('file::url') === false) { + return $this->url(); + } + + return $url; + case 'user': + return $this->url(); + default: + return $url; + } + } } diff --git a/kirby/src/Cms/FileActions.php b/kirby/src/Cms/FileActions.php index b06c686..e82171a 100644 --- a/kirby/src/Cms/FileActions.php +++ b/kirby/src/Cms/FileActions.php @@ -19,298 +19,295 @@ use Kirby\Form\Form; */ trait FileActions { - /** - * Renames the file without touching the extension - * The store is used to actually execute this. - * - * @param string $name - * @param bool $sanitize - * @return $this|static - * @throws \Kirby\Exception\LogicException - */ - public function changeName(string $name, bool $sanitize = true) - { - if ($sanitize === true) { - $name = F::safeName($name); - } + /** + * Renames the file without touching the extension + * The store is used to actually execute this. + * + * @param string $name + * @param bool $sanitize + * @return $this|static + * @throws \Kirby\Exception\LogicException + */ + public function changeName(string $name, bool $sanitize = true) + { + if ($sanitize === true) { + $name = F::safeName($name); + } - // don't rename if not necessary - if ($name === $this->name()) { - return $this; - } + // don't rename if not necessary + if ($name === $this->name()) { + return $this; + } - return $this->commit('changeName', ['file' => $this, 'name' => $name], function ($oldFile, $name) { - $newFile = $oldFile->clone([ - 'filename' => $name . '.' . $oldFile->extension(), - ]); + return $this->commit('changeName', ['file' => $this, 'name' => $name], function ($oldFile, $name) { + $newFile = $oldFile->clone([ + 'filename' => $name . '.' . $oldFile->extension(), + ]); - if ($oldFile->exists() === false) { - return $newFile; - } + if ($oldFile->exists() === false) { + return $newFile; + } - if ($newFile->exists() === true) { - throw new LogicException('The new file exists and cannot be overwritten'); - } + if ($newFile->exists() === true) { + throw new LogicException('The new file exists and cannot be overwritten'); + } - // remove the lock of the old file - if ($lock = $oldFile->lock()) { - $lock->remove(); - } + // remove the lock of the old file + if ($lock = $oldFile->lock()) { + $lock->remove(); + } - // remove all public versions - $oldFile->unpublish(); + // remove all public versions + $oldFile->unpublish(); - // rename the main file - F::move($oldFile->root(), $newFile->root()); + // rename the main file + F::move($oldFile->root(), $newFile->root()); - if ($newFile->kirby()->multilang() === true) { - foreach ($newFile->translations() as $translation) { - $translationCode = $translation->code(); + if ($newFile->kirby()->multilang() === true) { + foreach ($newFile->translations() as $translation) { + $translationCode = $translation->code(); - // rename the content file - F::move($oldFile->contentFile($translationCode), $newFile->contentFile($translationCode)); - } - } else { - // rename the content file - F::move($oldFile->contentFile(), $newFile->contentFile()); - } + // rename the content file + F::move($oldFile->contentFile($translationCode), $newFile->contentFile($translationCode)); + } + } else { + // rename the content file + F::move($oldFile->contentFile(), $newFile->contentFile()); + } - $newFile->parent()->files()->remove($oldFile->id()); - $newFile->parent()->files()->set($newFile->id(), $newFile); + $newFile->parent()->files()->remove($oldFile->id()); + $newFile->parent()->files()->set($newFile->id(), $newFile); - return $newFile; - }); - } + return $newFile; + }); + } - /** - * Changes the file's sorting number in the meta file - * - * @param int $sort - * @return static - */ - public function changeSort(int $sort) - { - return $this->commit( - 'changeSort', - ['file' => $this, 'position' => $sort], - fn ($file, $sort) => $file->save(['sort' => $sort]) - ); - } + /** + * Changes the file's sorting number in the meta file + * + * @param int $sort + * @return static + */ + public function changeSort(int $sort) + { + return $this->commit( + 'changeSort', + ['file' => $this, 'position' => $sort], + fn ($file, $sort) => $file->save(['sort' => $sort]) + ); + } - /** - * Commits a file action, by following these steps - * - * 1. checks the action rules - * 2. sends the before hook - * 3. commits the store action - * 4. sends the after hook - * 5. returns the result - * - * @param string $action - * @param array $arguments - * @param Closure $callback - * @return mixed - */ - protected function commit(string $action, array $arguments, Closure $callback) - { - $old = $this->hardcopy(); - $kirby = $this->kirby(); - $argumentValues = array_values($arguments); + /** + * Commits a file action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the store action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param array $arguments + * @param Closure $callback + * @return mixed + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); - $this->rules()->$action(...$argumentValues); - $kirby->trigger('file.' . $action . ':before', $arguments); + $this->rules()->$action(...$argumentValues); + $kirby->trigger('file.' . $action . ':before', $arguments); - $result = $callback(...$argumentValues); + $result = $callback(...$argumentValues); - if ($action === 'create') { - $argumentsAfter = ['file' => $result]; - } elseif ($action === 'delete') { - $argumentsAfter = ['status' => $result, 'file' => $old]; - } else { - $argumentsAfter = ['newFile' => $result, 'oldFile' => $old]; - } - $kirby->trigger('file.' . $action . ':after', $argumentsAfter); + if ($action === 'create') { + $argumentsAfter = ['file' => $result]; + } elseif ($action === 'delete') { + $argumentsAfter = ['status' => $result, 'file' => $old]; + } else { + $argumentsAfter = ['newFile' => $result, 'oldFile' => $old]; + } + $kirby->trigger('file.' . $action . ':after', $argumentsAfter); - $kirby->cache('pages')->flush(); - return $result; - } + $kirby->cache('pages')->flush(); + return $result; + } - /** - * Copy the file to the given page - * - * @param \Kirby\Cms\Page $page - * @return \Kirby\Cms\File - */ - public function copy(Page $page) - { - F::copy($this->root(), $page->root() . '/' . $this->filename()); + /** + * Copy the file to the given page + * + * @param \Kirby\Cms\Page $page + * @return \Kirby\Cms\File + */ + public function copy(Page $page) + { + F::copy($this->root(), $page->root() . '/' . $this->filename()); - if ($this->kirby()->multilang() === true) { - foreach ($this->kirby()->languages() as $language) { - $contentFile = $this->contentFile($language->code()); - F::copy($contentFile, $page->root() . '/' . basename($contentFile)); - } - } else { - $contentFile = $this->contentFile(); - F::copy($contentFile, $page->root() . '/' . basename($contentFile)); - } + if ($this->kirby()->multilang() === true) { + foreach ($this->kirby()->languages() as $language) { + $contentFile = $this->contentFile($language->code()); + F::copy($contentFile, $page->root() . '/' . basename($contentFile)); + } + } else { + $contentFile = $this->contentFile(); + F::copy($contentFile, $page->root() . '/' . basename($contentFile)); + } - return $page->clone()->file($this->filename()); - } + return $page->clone()->file($this->filename()); + } - /** - * Creates a new file on disk and returns the - * File object. The store is used to handle file - * writing, so it can be replaced by any other - * way of generating files. - * - * @param array $props - * @return static - * @throws \Kirby\Exception\InvalidArgumentException - * @throws \Kirby\Exception\LogicException - */ - public static function create(array $props) - { - if (isset($props['source'], $props['parent']) === false) { - throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File'); - } + /** + * Creates a new file on disk and returns the + * File object. The store is used to handle file + * writing, so it can be replaced by any other + * way of generating files. + * + * @param array $props + * @return static + * @throws \Kirby\Exception\InvalidArgumentException + * @throws \Kirby\Exception\LogicException + */ + public static function create(array $props) + { + if (isset($props['source'], $props['parent']) === false) { + throw new InvalidArgumentException('Please provide the "source" and "parent" props for the File'); + } - // prefer the filename from the props - $props['filename'] = F::safeName($props['filename'] ?? basename($props['source'])); + // prefer the filename from the props + $props['filename'] = F::safeName($props['filename'] ?? basename($props['source'])); - $props['model'] = strtolower($props['template'] ?? 'default'); + $props['model'] = strtolower($props['template'] ?? 'default'); - // create the basic file and a test upload object - $file = static::factory($props); - $upload = $file->asset($props['source']); + // create the basic file and a test upload object + $file = static::factory($props); + $upload = $file->asset($props['source']); - // create a form for the file - $form = Form::for($file, [ - 'values' => $props['content'] ?? [] - ]); + // create a form for the file + $form = Form::for($file, [ + 'values' => $props['content'] ?? [] + ]); - // inject the content - $file = $file->clone(['content' => $form->strings(true)]); + // inject the content + $file = $file->clone(['content' => $form->strings(true)]); - // run the hook - return $file->commit('create', compact('file', 'upload'), function ($file, $upload) { + // run the hook + return $file->commit('create', compact('file', 'upload'), function ($file, $upload) { + // delete all public versions + $file->unpublish(); - // delete all public versions - $file->unpublish(); + // overwrite the original + if (F::copy($upload->root(), $file->root(), true) !== true) { + throw new LogicException('The file could not be created'); + } - // overwrite the original - if (F::copy($upload->root(), $file->root(), true) !== true) { - throw new LogicException('The file could not be created'); - } + // always create pages in the default language + if ($file->kirby()->multilang() === true) { + $languageCode = $file->kirby()->defaultLanguage()->code(); + } else { + $languageCode = null; + } - // always create pages in the default language - if ($file->kirby()->multilang() === true) { - $languageCode = $file->kirby()->defaultLanguage()->code(); - } else { - $languageCode = null; - } + // store the content if necessary + $file->save($file->content()->toArray(), $languageCode); - // store the content if necessary - $file->save($file->content()->toArray(), $languageCode); + // add the file to the list of siblings + $file->siblings()->append($file->id(), $file); - // add the file to the list of siblings - $file->siblings()->append($file->id(), $file); + // return a fresh clone + return $file->clone(); + }); + } - // return a fresh clone - return $file->clone(); - }); - } + /** + * Deletes the file. The store is used to + * manipulate the filesystem or whatever you prefer. + * + * @return bool + */ + public function delete(): bool + { + return $this->commit('delete', ['file' => $this], function ($file) { + // remove all versions in the media folder + $file->unpublish(); - /** - * Deletes the file. The store is used to - * manipulate the filesystem or whatever you prefer. - * - * @return bool - */ - public function delete(): bool - { - return $this->commit('delete', ['file' => $this], function ($file) { + // remove the lock of the old file + if ($lock = $file->lock()) { + $lock->remove(); + } - // remove all versions in the media folder - $file->unpublish(); + if ($file->kirby()->multilang() === true) { + foreach ($file->translations() as $translation) { + F::remove($file->contentFile($translation->code())); + } + } else { + F::remove($file->contentFile()); + } - // remove the lock of the old file - if ($lock = $file->lock()) { - $lock->remove(); - } + F::remove($file->root()); - if ($file->kirby()->multilang() === true) { - foreach ($file->translations() as $translation) { - F::remove($file->contentFile($translation->code())); - } - } else { - F::remove($file->contentFile()); - } + // remove the file from the sibling collection + $file->parent()->files()->remove($file); - F::remove($file->root()); + return true; + }); + } - // remove the file from the sibling collection - $file->parent()->files()->remove($file); + /** + * Move the file to the public media folder + * if it's not already there. + * + * @return $this + */ + public function publish() + { + Media::publish($this, $this->mediaRoot()); + return $this; + } - return true; - }); - } + /** + * Replaces the file. The source must + * be an absolute path to a file or a Url. + * The store handles the replacement so it + * finally decides what it will support as + * source. + * + * @param string $source + * @return static + * @throws \Kirby\Exception\LogicException + */ + public function replace(string $source) + { + $file = $this->clone(); - /** - * Move the file to the public media folder - * if it's not already there. - * - * @return $this - */ - public function publish() - { - Media::publish($this, $this->mediaRoot()); - return $this; - } + $arguments = [ + 'file' => $file, + 'upload' => $file->asset($source) + ]; - /** - * Replaces the file. The source must - * be an absolute path to a file or a Url. - * The store handles the replacement so it - * finally decides what it will support as - * source. - * - * @param string $source - * @return static - * @throws \Kirby\Exception\LogicException - */ - public function replace(string $source) - { - $file = $this->clone(); + return $this->commit('replace', $arguments, function ($file, $upload) { + // delete all public versions + $file->unpublish(); - $arguments = [ - 'file' => $file, - 'upload' => $file->asset($source) - ]; + // overwrite the original + if (F::copy($upload->root(), $file->root(), true) !== true) { + throw new LogicException('The file could not be created'); + } - return $this->commit('replace', $arguments, function ($file, $upload) { + // return a fresh clone + return $file->clone(); + }); + } - // delete all public versions - $file->unpublish(); - - // overwrite the original - if (F::copy($upload->root(), $file->root(), true) !== true) { - throw new LogicException('The file could not be created'); - } - - // return a fresh clone - return $file->clone(); - }); - } - - /** - * Remove all public versions of this file - * - * @return $this - */ - public function unpublish() - { - Media::unpublish($this->parent()->mediaRoot(), $this); - return $this; - } + /** + * Remove all public versions of this file + * + * @return $this + */ + public function unpublish() + { + Media::unpublish($this->parent()->mediaRoot(), $this); + return $this; + } } diff --git a/kirby/src/Cms/FileBlueprint.php b/kirby/src/Cms/FileBlueprint.php index 3cb1062..aee9b8b 100644 --- a/kirby/src/Cms/FileBlueprint.php +++ b/kirby/src/Cms/FileBlueprint.php @@ -17,172 +17,172 @@ use Kirby\Toolkit\Str; */ class FileBlueprint extends Blueprint { - /** - * `true` if the default accepted - * types are being used - * - * @var bool - */ - protected $defaultTypes = false; + /** + * `true` if the default accepted + * types are being used + * + * @var bool + */ + protected $defaultTypes = false; - public function __construct(array $props) - { - parent::__construct($props); + public function __construct(array $props) + { + parent::__construct($props); - // normalize all available page options - $this->props['options'] = $this->normalizeOptions( - $this->props['options'] ?? true, - // defaults - [ - 'changeName' => null, - 'create' => null, - 'delete' => null, - 'read' => null, - 'replace' => null, - 'update' => null, - ] - ); + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $this->props['options'] ?? true, + // defaults + [ + 'changeName' => null, + 'create' => null, + 'delete' => null, + 'read' => null, + 'replace' => null, + 'update' => null, + ] + ); - // normalize the accept settings - $this->props['accept'] = $this->normalizeAccept($this->props['accept'] ?? []); - } + // normalize the accept settings + $this->props['accept'] = $this->normalizeAccept($this->props['accept'] ?? []); + } - /** - * @return array - */ - public function accept(): array - { - return $this->props['accept']; - } + /** + * @return array + */ + public function accept(): array + { + return $this->props['accept']; + } - /** - * Returns the list of all accepted MIME types for - * file upload or `*` if all MIME types are allowed - * - * @return string - */ - public function acceptMime(): string - { - // don't disclose the specific default types - if ($this->defaultTypes === true) { - return '*'; - } + /** + * Returns the list of all accepted MIME types for + * file upload or `*` if all MIME types are allowed + * + * @return string + */ + public function acceptMime(): string + { + // don't disclose the specific default types + if ($this->defaultTypes === true) { + return '*'; + } - $accept = $this->accept(); - $restrictions = []; + $accept = $this->accept(); + $restrictions = []; - if (is_array($accept['mime']) === true) { - $restrictions[] = $accept['mime']; - } else { - // only fall back to the extension or type if - // no explicit MIME types were defined - // (allows to set custom MIME types for the frontend - // check but still restrict the extension and/or type) + if (is_array($accept['mime']) === true) { + $restrictions[] = $accept['mime']; + } else { + // only fall back to the extension or type if + // no explicit MIME types were defined + // (allows to set custom MIME types for the frontend + // check but still restrict the extension and/or type) - if (is_array($accept['extension']) === true) { - // determine the main MIME type for each extension - $restrictions[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $accept['extension']); - } + if (is_array($accept['extension']) === true) { + // determine the main MIME type for each extension + $restrictions[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $accept['extension']); + } - if (is_array($accept['type']) === true) { - // determine the MIME types of each file type - $mimes = []; - foreach ($accept['type'] as $type) { - if ($extensions = F::typeToExtensions($type)) { - $mimes[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $extensions); - } - } + if (is_array($accept['type']) === true) { + // determine the MIME types of each file type + $mimes = []; + foreach ($accept['type'] as $type) { + if ($extensions = F::typeToExtensions($type)) { + $mimes[] = array_map(['Kirby\Filesystem\Mime', 'fromExtension'], $extensions); + } + } - $restrictions[] = array_merge(...$mimes); - } - } + $restrictions[] = array_merge(...$mimes); + } + } - if ($restrictions !== []) { - if (count($restrictions) > 1) { - // only return the MIME types that are allowed by all restrictions - $mimes = array_intersect(...$restrictions); - } else { - $mimes = $restrictions[0]; - } + if ($restrictions !== []) { + if (count($restrictions) > 1) { + // only return the MIME types that are allowed by all restrictions + $mimes = array_intersect(...$restrictions); + } else { + $mimes = $restrictions[0]; + } - // filter out empty MIME types and duplicates - return implode(', ', array_filter(array_unique($mimes))); - } + // filter out empty MIME types and duplicates + return implode(', ', array_filter(array_unique($mimes))); + } - // no restrictions, accept everything - return '*'; - } + // no restrictions, accept everything + return '*'; + } - /** - * @param mixed $accept - * @return array - */ - protected function normalizeAccept($accept = null): array - { - if (is_string($accept) === true) { - $accept = [ - 'mime' => $accept - ]; - } elseif ($accept === true) { - // explicitly no restrictions at all - $accept = [ - 'mime' => null - ]; - } elseif (empty($accept) === true) { - // no custom restrictions - $accept = []; - } + /** + * @param mixed $accept + * @return array + */ + protected function normalizeAccept($accept = null): array + { + if (is_string($accept) === true) { + $accept = [ + 'mime' => $accept + ]; + } elseif ($accept === true) { + // explicitly no restrictions at all + $accept = [ + 'mime' => null + ]; + } elseif (empty($accept) === true) { + // no custom restrictions + $accept = []; + } - $accept = array_change_key_case($accept); + $accept = array_change_key_case($accept); - $defaults = [ - 'extension' => null, - 'mime' => null, - 'maxheight' => null, - 'maxsize' => null, - 'maxwidth' => null, - 'minheight' => null, - 'minsize' => null, - 'minwidth' => null, - 'orientation' => null, - 'type' => null - ]; + $defaults = [ + 'extension' => null, + 'mime' => null, + 'maxheight' => null, + 'maxsize' => null, + 'maxwidth' => null, + 'minheight' => null, + 'minsize' => null, + 'minwidth' => null, + 'orientation' => null, + 'type' => null + ]; - // default type restriction if none are configured; - // this ensures that no unexpected files are uploaded - if ( - array_key_exists('mime', $accept) === false && - array_key_exists('extension', $accept) === false && - array_key_exists('type', $accept) === false - ) { - $defaults['type'] = ['image', 'document', 'archive', 'audio', 'video']; - $this->defaultTypes = true; - } + // default type restriction if none are configured; + // this ensures that no unexpected files are uploaded + if ( + array_key_exists('mime', $accept) === false && + array_key_exists('extension', $accept) === false && + array_key_exists('type', $accept) === false + ) { + $defaults['type'] = ['image', 'document', 'archive', 'audio', 'video']; + $this->defaultTypes = true; + } - $accept = array_merge($defaults, $accept); + $accept = array_merge($defaults, $accept); - // normalize the MIME, extension and type from strings into arrays - if (is_string($accept['mime']) === true) { - $accept['mime'] = array_map( - fn ($mime) => $mime['value'], - Str::accepted($accept['mime']) - ); - } + // normalize the MIME, extension and type from strings into arrays + if (is_string($accept['mime']) === true) { + $accept['mime'] = array_map( + fn ($mime) => $mime['value'], + Str::accepted($accept['mime']) + ); + } - if (is_string($accept['extension']) === true) { - $accept['extension'] = array_map( - 'trim', - explode(',', $accept['extension']) - ); - } + if (is_string($accept['extension']) === true) { + $accept['extension'] = array_map( + 'trim', + explode(',', $accept['extension']) + ); + } - if (is_string($accept['type']) === true) { - $accept['type'] = array_map( - 'trim', - explode(',', $accept['type']) - ); - } + if (is_string($accept['type']) === true) { + $accept['type'] = array_map( + 'trim', + explode(',', $accept['type']) + ); + } - return $accept; - } + return $accept; + } } diff --git a/kirby/src/Cms/FileModifications.php b/kirby/src/Cms/FileModifications.php index b9b6912..9e2ca70 100644 --- a/kirby/src/Cms/FileModifications.php +++ b/kirby/src/Cms/FileModifications.php @@ -15,200 +15,200 @@ use Kirby\Exception\InvalidArgumentException; */ trait FileModifications { - /** - * Blurs the image by the given amount of pixels - * - * @param bool $pixels - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - */ - public function blur($pixels = true) - { - return $this->thumb(['blur' => $pixels]); - } + /** + * Blurs the image by the given amount of pixels + * + * @param bool $pixels + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function blur($pixels = true) + { + return $this->thumb(['blur' => $pixels]); + } - /** - * Converts the image to black and white - * - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - */ - public function bw() - { - return $this->thumb(['grayscale' => true]); - } + /** + * Converts the image to black and white + * + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function bw() + { + return $this->thumb(['grayscale' => true]); + } - /** - * Crops the image by the given width and height - * - * @param int $width - * @param int|null $height - * @param string|array $options - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - */ - public function crop(int $width, int $height = null, $options = null) - { - $quality = null; - $crop = 'center'; + /** + * Crops the image by the given width and height + * + * @param int $width + * @param int|null $height + * @param string|array $options + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function crop(int $width, int $height = null, $options = null) + { + $quality = null; + $crop = 'center'; - if (is_int($options) === true) { - $quality = $options; - } elseif (is_string($options)) { - $crop = $options; - } elseif (is_a($options, 'Kirby\Cms\Field') === true) { - $crop = $options->value(); - } elseif (is_array($options)) { - $quality = $options['quality'] ?? $quality; - $crop = $options['crop'] ?? $crop; - } + if (is_int($options) === true) { + $quality = $options; + } elseif (is_string($options)) { + $crop = $options; + } elseif (is_a($options, 'Kirby\Cms\Field') === true) { + $crop = $options->value(); + } elseif (is_array($options)) { + $quality = $options['quality'] ?? $quality; + $crop = $options['crop'] ?? $crop; + } - return $this->thumb([ - 'width' => $width, - 'height' => $height, - 'quality' => $quality, - 'crop' => $crop - ]); - } + return $this->thumb([ + 'width' => $width, + 'height' => $height, + 'quality' => $quality, + 'crop' => $crop + ]); + } - /** - * Alias for File::bw() - * - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - */ - public function grayscale() - { - return $this->thumb(['grayscale' => true]); - } + /** + * Alias for File::bw() + * + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function grayscale() + { + return $this->thumb(['grayscale' => true]); + } - /** - * Alias for File::bw() - * - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - */ - public function greyscale() - { - return $this->thumb(['grayscale' => true]); - } + /** + * Alias for File::bw() + * + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function greyscale() + { + return $this->thumb(['grayscale' => true]); + } - /** - * Sets the JPEG compression quality - * - * @param int $quality - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - */ - public function quality(int $quality) - { - return $this->thumb(['quality' => $quality]); - } + /** + * Sets the JPEG compression quality + * + * @param int $quality + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + */ + public function quality(int $quality) + { + return $this->thumb(['quality' => $quality]); + } - /** - * Resizes the file with the given width and height - * while keeping the aspect ratio. - * - * @param int|null $width - * @param int|null $height - * @param int|null $quality - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function resize(int $width = null, int $height = null, int $quality = null) - { - return $this->thumb([ - 'width' => $width, - 'height' => $height, - 'quality' => $quality - ]); - } + /** + * Resizes the file with the given width and height + * while keeping the aspect ratio. + * + * @param int|null $width + * @param int|null $height + * @param int|null $quality + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function resize(int $width = null, int $height = null, int $quality = null) + { + return $this->thumb([ + 'width' => $width, + 'height' => $height, + 'quality' => $quality + ]); + } - /** - * Create a srcset definition for the given sizes - * Sizes can be defined as a simple array. They can - * also be set up in the config with the thumbs.srcsets option. - * @since 3.1.0 - * - * @param array|string|null $sizes - * @return string|null - */ - public function srcset($sizes = null): ?string - { - if (empty($sizes) === true) { - $sizes = $this->kirby()->option('thumbs.srcsets.default', []); - } + /** + * Create a srcset definition for the given sizes + * Sizes can be defined as a simple array. They can + * also be set up in the config with the thumbs.srcsets option. + * @since 3.1.0 + * + * @param array|string|null $sizes + * @return string|null + */ + public function srcset($sizes = null): ?string + { + if (empty($sizes) === true) { + $sizes = $this->kirby()->option('thumbs.srcsets.default', []); + } - if (is_string($sizes) === true) { - $sizes = $this->kirby()->option('thumbs.srcsets.' . $sizes, []); - } + if (is_string($sizes) === true) { + $sizes = $this->kirby()->option('thumbs.srcsets.' . $sizes, []); + } - if (is_array($sizes) === false || empty($sizes) === true) { - return null; - } + if (is_array($sizes) === false || empty($sizes) === true) { + return null; + } - $set = []; + $set = []; - foreach ($sizes as $key => $value) { - if (is_array($value)) { - $options = $value; - $condition = $key; - } elseif (is_string($value) === true) { - $options = [ - 'width' => $key - ]; - $condition = $value; - } else { - $options = [ - 'width' => $value - ]; - $condition = $value . 'w'; - } + foreach ($sizes as $key => $value) { + if (is_array($value)) { + $options = $value; + $condition = $key; + } elseif (is_string($value) === true) { + $options = [ + 'width' => $key + ]; + $condition = $value; + } else { + $options = [ + 'width' => $value + ]; + $condition = $value . 'w'; + } - $set[] = $this->thumb($options)->url() . ' ' . $condition; - } + $set[] = $this->thumb($options)->url() . ' ' . $condition; + } - return implode(', ', $set); - } + return implode(', ', $set); + } - /** - * Creates a modified version of images - * The media manager takes care of generating - * those modified versions and putting them - * in the right place. This is normally the - * `/media` folder of your installation, but - * could potentially also be a CDN or any other - * place. - * - * @param array|null|string $options - * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function thumb($options = null) - { - // thumb presets - if (empty($options) === true) { - $options = $this->kirby()->option('thumbs.presets.default'); - } elseif (is_string($options) === true) { - $options = $this->kirby()->option('thumbs.presets.' . $options); - } + /** + * Creates a modified version of images + * The media manager takes care of generating + * those modified versions and putting them + * in the right place. This is normally the + * `/media` folder of your installation, but + * could potentially also be a CDN or any other + * place. + * + * @param array|null|string $options + * @return \Kirby\Cms\FileVersion|\Kirby\Cms\File + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function thumb($options = null) + { + // thumb presets + if (empty($options) === true) { + $options = $this->kirby()->option('thumbs.presets.default'); + } elseif (is_string($options) === true) { + $options = $this->kirby()->option('thumbs.presets.' . $options); + } - if (empty($options) === true || is_array($options) === false) { - return $this; - } + if (empty($options) === true || is_array($options) === false) { + return $this; + } - // fallback to global config options - if (isset($options['format']) === false) { - if ($format = $this->kirby()->option('thumbs.format')) { - $options['format'] = $format; - } - } + // fallback to global config options + if (isset($options['format']) === false) { + if ($format = $this->kirby()->option('thumbs.format')) { + $options['format'] = $format; + } + } - $component = $this->kirby()->component('file::version'); - $result = $component($this->kirby(), $this, $options); + $component = $this->kirby()->component('file::version'); + $result = $component($this->kirby(), $this, $options); - if ( - is_a($result, 'Kirby\Cms\FileVersion') === false && - is_a($result, 'Kirby\Cms\File') === false && - is_a($result, 'Kirby\Filesystem\Asset') === false - ) { - throw new InvalidArgumentException('The file::version component must return a File, FileVersion or Asset object'); - } + if ( + is_a($result, 'Kirby\Cms\FileVersion') === false && + is_a($result, 'Kirby\Cms\File') === false && + is_a($result, 'Kirby\Filesystem\Asset') === false + ) { + throw new InvalidArgumentException('The file::version component must return a File, FileVersion or Asset object'); + } - return $result; - } + return $result; + } } diff --git a/kirby/src/Cms/FilePermissions.php b/kirby/src/Cms/FilePermissions.php index 674a058..e296de5 100644 --- a/kirby/src/Cms/FilePermissions.php +++ b/kirby/src/Cms/FilePermissions.php @@ -13,5 +13,5 @@ namespace Kirby\Cms; */ class FilePermissions extends ModelPermissions { - protected $category = 'files'; + protected $category = 'files'; } diff --git a/kirby/src/Cms/FilePicker.php b/kirby/src/Cms/FilePicker.php index b81ec9d..ea30ce1 100644 --- a/kirby/src/Cms/FilePicker.php +++ b/kirby/src/Cms/FilePicker.php @@ -17,58 +17,58 @@ use Kirby\Exception\InvalidArgumentException; */ class FilePicker extends Picker { - /** - * Extends the basic defaults - * - * @return array - */ - public function defaults(): array - { - $defaults = parent::defaults(); - $defaults['text'] = '{{ file.filename }}'; + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + $defaults = parent::defaults(); + $defaults['text'] = '{{ file.filename }}'; - return $defaults; - } + return $defaults; + } - /** - * Search all files for the picker - * - * @return \Kirby\Cms\Files|null - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function items() - { - $model = $this->options['model']; + /** + * Search all files for the picker + * + * @return \Kirby\Cms\Files|null + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function items() + { + $model = $this->options['model']; - // find the right default query - if (empty($this->options['query']) === false) { - $query = $this->options['query']; - } elseif (is_a($model, 'Kirby\Cms\File') === true) { - $query = 'file.siblings'; - } else { - $query = $model::CLASS_ALIAS . '.files'; - } + // find the right default query + if (empty($this->options['query']) === false) { + $query = $this->options['query']; + } elseif (is_a($model, 'Kirby\Cms\File') === true) { + $query = 'file.siblings'; + } else { + $query = $model::CLASS_ALIAS . '.files'; + } - // fetch all files for the picker - $files = $model->query($query); + // fetch all files for the picker + $files = $model->query($query); - // help mitigate some typical query usage issues - // by converting site and page objects to proper - // pages by returning their children - if (is_a($files, 'Kirby\Cms\Site') === true) { - $files = $files->files(); - } elseif (is_a($files, 'Kirby\Cms\Page') === true) { - $files = $files->files(); - } elseif (is_a($files, 'Kirby\Cms\User') === true) { - $files = $files->files(); - } elseif (is_a($files, 'Kirby\Cms\Files') === false) { - throw new InvalidArgumentException('Your query must return a set of files'); - } + // help mitigate some typical query usage issues + // by converting site and page objects to proper + // pages by returning their children + if (is_a($files, 'Kirby\Cms\Site') === true) { + $files = $files->files(); + } elseif (is_a($files, 'Kirby\Cms\Page') === true) { + $files = $files->files(); + } elseif (is_a($files, 'Kirby\Cms\User') === true) { + $files = $files->files(); + } elseif (is_a($files, 'Kirby\Cms\Files') === false) { + throw new InvalidArgumentException('Your query must return a set of files'); + } - // search - $files = $this->search($files); + // search + $files = $this->search($files); - // paginate - return $this->paginate($files); - } + // paginate + return $this->paginate($files); + } } diff --git a/kirby/src/Cms/FileRules.php b/kirby/src/Cms/FileRules.php index 244119c..09c6b53 100644 --- a/kirby/src/Cms/FileRules.php +++ b/kirby/src/Cms/FileRules.php @@ -20,300 +20,315 @@ use Kirby\Toolkit\V; */ class FileRules { - /** - * Validates if the filename can be changed - * - * @param \Kirby\Cms\File $file - * @param string $name - * @return bool - * @throws \Kirby\Exception\DuplicateException If a file with this name exists - * @throws \Kirby\Exception\PermissionException If the user is not allowed to rename the file - */ - public static function changeName(File $file, string $name): bool - { - if ($file->permissions()->changeName() !== true) { - throw new PermissionException([ - 'key' => 'file.changeName.permission', - 'data' => ['filename' => $file->filename()] - ]); - } + /** + * Validates if the filename can be changed + * + * @param \Kirby\Cms\File $file + * @param string $name + * @return bool + * @throws \Kirby\Exception\DuplicateException If a file with this name exists + * @throws \Kirby\Exception\PermissionException If the user is not allowed to rename the file + */ + public static function changeName(File $file, string $name): bool + { + if ($file->permissions()->changeName() !== true) { + throw new PermissionException([ + 'key' => 'file.changeName.permission', + 'data' => ['filename' => $file->filename()] + ]); + } - if (Str::length($name) === 0) { - throw new InvalidArgumentException([ - 'key' => 'file.changeName.empty' - ]); - } + if (Str::length($name) === 0) { + throw new InvalidArgumentException([ + 'key' => 'file.changeName.empty' + ]); + } - $parent = $file->parent(); - $duplicate = $parent->files()->not($file)->findBy('filename', $name . '.' . $file->extension()); + $parent = $file->parent(); + $duplicate = $parent->files()->not($file)->findBy('filename', $name . '.' . $file->extension()); - if ($duplicate) { - throw new DuplicateException([ - 'key' => 'file.duplicate', - 'data' => ['filename' => $duplicate->filename()] - ]); - } + if ($duplicate) { + throw new DuplicateException([ + 'key' => 'file.duplicate', + 'data' => ['filename' => $duplicate->filename()] + ]); + } - return true; - } + return true; + } - /** - * Validates if the file can be sorted - * - * @param \Kirby\Cms\File $file - * @param int $sort - * @return bool - */ - public static function changeSort(File $file, int $sort): bool - { - return true; - } + /** + * Validates if the file can be sorted + * + * @param \Kirby\Cms\File $file + * @param int $sort + * @return bool + */ + public static function changeSort(File $file, int $sort): bool + { + return true; + } - /** - * Validates if the file can be created - * - * @param \Kirby\Cms\File $file - * @param \Kirby\Filesystem\File $upload - * @return bool - * @throws \Kirby\Exception\DuplicateException If a file with the same name exists - * @throws \Kirby\Exception\PermissionException If the user is not allowed to create the file - */ - public static function create(File $file, BaseFile $upload): bool - { - if ($file->exists() === true) { - if ($file->sha1() !== $upload->sha1()) { - throw new DuplicateException([ - 'key' => 'file.duplicate', - 'data' => [ - 'filename' => $file->filename() - ] - ]); - } - } + /** + * Validates if the file can be created + * + * @param \Kirby\Cms\File $file + * @param \Kirby\Filesystem\File $upload + * @return bool + * @throws \Kirby\Exception\DuplicateException If a file with the same name exists + * @throws \Kirby\Exception\PermissionException If the user is not allowed to create the file + */ + public static function create(File $file, BaseFile $upload): bool + { + // We want to ensure that we are not creating duplicate files. + // If a file with the same name already exists + if ($file->exists() === true) { + // $file will be based on the props of the new file, + // to compare templates, we need to get the props of + // the already existing file from meta content file + $existing = $file->parent()->file($file->filename()); - if ($file->permissions()->create() !== true) { - throw new PermissionException('The file cannot be created'); - } + // if the new upload is the exact same file + // and uses the same template, we can continue + if ( + $file->sha1() === $upload->sha1() && + $file->template() === $existing->template() + ) { + return true; + } - static::validFile($file, $upload->mime()); + // otherwise throw an error for duplicate file + throw new DuplicateException([ + 'key' => 'file.duplicate', + 'data' => [ + 'filename' => $file->filename() + ] + ]); + } - $upload->match($file->blueprint()->accept()); - $upload->validateContents(true); + if ($file->permissions()->create() !== true) { + throw new PermissionException('The file cannot be created'); + } - return true; - } + static::validFile($file, $upload->mime()); - /** - * Validates if the file can be deleted - * - * @param \Kirby\Cms\File $file - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the file - */ - public static function delete(File $file): bool - { - if ($file->permissions()->delete() !== true) { - throw new PermissionException('The file cannot be deleted'); - } + $upload->match($file->blueprint()->accept()); + $upload->validateContents(true); - return true; - } + return true; + } - /** - * Validates if the file can be replaced - * - * @param \Kirby\Cms\File $file - * @param \Kirby\Filesystem\File $upload - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to replace the file - * @throws \Kirby\Exception\InvalidArgumentException If the file type of the new file is different - */ - public static function replace(File $file, BaseFile $upload): bool - { - if ($file->permissions()->replace() !== true) { - throw new PermissionException('The file cannot be replaced'); - } + /** + * Validates if the file can be deleted + * + * @param \Kirby\Cms\File $file + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the file + */ + public static function delete(File $file): bool + { + if ($file->permissions()->delete() !== true) { + throw new PermissionException('The file cannot be deleted'); + } - static::validMime($file, $upload->mime()); + return true; + } - if ( - (string)$upload->mime() !== (string)$file->mime() && - (string)$upload->extension() !== (string)$file->extension() - ) { - throw new InvalidArgumentException([ - 'key' => 'file.mime.differs', - 'data' => ['mime' => $file->mime()] - ]); - } + /** + * Validates if the file can be replaced + * + * @param \Kirby\Cms\File $file + * @param \Kirby\Filesystem\File $upload + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to replace the file + * @throws \Kirby\Exception\InvalidArgumentException If the file type of the new file is different + */ + public static function replace(File $file, BaseFile $upload): bool + { + if ($file->permissions()->replace() !== true) { + throw new PermissionException('The file cannot be replaced'); + } - $upload->match($file->blueprint()->accept()); - $upload->validateContents(true); + static::validMime($file, $upload->mime()); - return true; - } + if ( + (string)$upload->mime() !== (string)$file->mime() && + (string)$upload->extension() !== (string)$file->extension() + ) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.differs', + 'data' => ['mime' => $file->mime()] + ]); + } - /** - * Validates if the file can be updated - * - * @param \Kirby\Cms\File $file - * @param array $content - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the file - */ - public static function update(File $file, array $content = []): bool - { - if ($file->permissions()->update() !== true) { - throw new PermissionException('The file cannot be updated'); - } + $upload->match($file->blueprint()->accept()); + $upload->validateContents(true); - return true; - } + return true; + } - /** - * Validates the file extension - * - * @param \Kirby\Cms\File $file - * @param string $extension - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the extension is missing or forbidden - */ - public static function validExtension(File $file, string $extension): bool - { - // make it easier to compare the extension - $extension = strtolower($extension); + /** + * Validates if the file can be updated + * + * @param \Kirby\Cms\File $file + * @param array $content + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the file + */ + public static function update(File $file, array $content = []): bool + { + if ($file->permissions()->update() !== true) { + throw new PermissionException('The file cannot be updated'); + } - if (empty($extension) === true) { - throw new InvalidArgumentException([ - 'key' => 'file.extension.missing', - 'data' => ['filename' => $file->filename()] - ]); - } + return true; + } - if ( - Str::contains($extension, 'php') !== false || - Str::contains($extension, 'phar') !== false || - Str::contains($extension, 'phtml') !== false - ) { - throw new InvalidArgumentException([ - 'key' => 'file.type.forbidden', - 'data' => ['type' => 'PHP'] - ]); - } + /** + * Validates the file extension + * + * @param \Kirby\Cms\File $file + * @param string $extension + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the extension is missing or forbidden + */ + public static function validExtension(File $file, string $extension): bool + { + // make it easier to compare the extension + $extension = strtolower($extension); - if (Str::contains($extension, 'htm') !== false) { - throw new InvalidArgumentException([ - 'key' => 'file.type.forbidden', - 'data' => ['type' => 'HTML'] - ]); - } + if (empty($extension) === true) { + throw new InvalidArgumentException([ + 'key' => 'file.extension.missing', + 'data' => ['filename' => $file->filename()] + ]); + } - if (V::in($extension, ['exe', App::instance()->contentExtension()]) !== false) { - throw new InvalidArgumentException([ - 'key' => 'file.extension.forbidden', - 'data' => ['extension' => $extension] - ]); - } + if ( + Str::contains($extension, 'php') !== false || + Str::contains($extension, 'phar') !== false || + Str::contains($extension, 'phtml') !== false + ) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'PHP'] + ]); + } - return true; - } + if (Str::contains($extension, 'htm') !== false) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'HTML'] + ]); + } - /** - * Validates the extension, MIME type and filename - * - * @param \Kirby\Cms\File $file - * @param string|null|false $mime If not passed, the MIME type is detected from the file, - * if `false`, the MIME type is not validated for performance reasons - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the extension, MIME type or filename is missing or forbidden - */ - public static function validFile(File $file, $mime = null): bool - { - if ($mime === false) { - // request to skip the MIME check for performance reasons - $validMime = true; - } else { - $validMime = static::validMime($file, $mime ?? $file->mime()); - } + if (V::in($extension, ['exe', App::instance()->contentExtension()]) !== false) { + throw new InvalidArgumentException([ + 'key' => 'file.extension.forbidden', + 'data' => ['extension' => $extension] + ]); + } - return - $validMime && - static::validExtension($file, $file->extension()) && - static::validFilename($file, $file->filename()); - } + return true; + } - /** - * Validates the filename - * - * @param \Kirby\Cms\File $file - * @param string $filename - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the filename is missing or forbidden - */ - public static function validFilename(File $file, string $filename): bool - { - // make it easier to compare the filename - $filename = strtolower($filename); + /** + * Validates the extension, MIME type and filename + * + * @param \Kirby\Cms\File $file + * @param string|null|false $mime If not passed, the MIME type is detected from the file, + * if `false`, the MIME type is not validated for performance reasons + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the extension, MIME type or filename is missing or forbidden + */ + public static function validFile(File $file, $mime = null): bool + { + if ($mime === false) { + // request to skip the MIME check for performance reasons + $validMime = true; + } else { + $validMime = static::validMime($file, $mime ?? $file->mime()); + } - // check for missing filenames - if (empty($filename)) { - throw new InvalidArgumentException([ - 'key' => 'file.name.missing' - ]); - } + return + $validMime && + static::validExtension($file, $file->extension()) && + static::validFilename($file, $file->filename()); + } - // Block htaccess files - if (Str::startsWith($filename, '.ht')) { - throw new InvalidArgumentException([ - 'key' => 'file.type.forbidden', - 'data' => ['type' => 'Apache config'] - ]); - } + /** + * Validates the filename + * + * @param \Kirby\Cms\File $file + * @param string $filename + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the filename is missing or forbidden + */ + public static function validFilename(File $file, string $filename): bool + { + // make it easier to compare the filename + $filename = strtolower($filename); - // Block invisible files - if (Str::startsWith($filename, '.')) { - throw new InvalidArgumentException([ - 'key' => 'file.type.forbidden', - 'data' => ['type' => 'invisible'] - ]); - } + // check for missing filenames + if (empty($filename)) { + throw new InvalidArgumentException([ + 'key' => 'file.name.missing' + ]); + } - return true; - } + // Block htaccess files + if (Str::startsWith($filename, '.ht')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'Apache config'] + ]); + } - /** - * Validates the MIME type - * - * @param \Kirby\Cms\File $file - * @param string|null $mime - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the MIME type is missing or forbidden - */ - public static function validMime(File $file, string $mime = null): bool - { - // make it easier to compare the mime - $mime = strtolower($mime); + // Block invisible files + if (Str::startsWith($filename, '.')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'invisible'] + ]); + } - if (empty($mime)) { - throw new InvalidArgumentException([ - 'key' => 'file.mime.missing', - 'data' => ['filename' => $file->filename()] - ]); - } + return true; + } - if (Str::contains($mime, 'php')) { - throw new InvalidArgumentException([ - 'key' => 'file.type.forbidden', - 'data' => ['type' => 'PHP'] - ]); - } + /** + * Validates the MIME type + * + * @param \Kirby\Cms\File $file + * @param string|null $mime + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the MIME type is missing or forbidden + */ + public static function validMime(File $file, string $mime = null): bool + { + // make it easier to compare the mime + $mime = strtolower($mime); - if (V::in($mime, ['text/html', 'application/x-msdownload'])) { - throw new InvalidArgumentException([ - 'key' => 'file.mime.forbidden', - 'data' => ['mime' => $mime] - ]); - } + if (empty($mime)) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.missing', + 'data' => ['filename' => $file->filename()] + ]); + } - return true; - } + if (Str::contains($mime, 'php')) { + throw new InvalidArgumentException([ + 'key' => 'file.type.forbidden', + 'data' => ['type' => 'PHP'] + ]); + } + + if (V::in($mime, ['text/html', 'application/x-msdownload'])) { + throw new InvalidArgumentException([ + 'key' => 'file.mime.forbidden', + 'data' => ['mime' => $mime] + ]); + } + + return true; + } } diff --git a/kirby/src/Cms/FileVersion.php b/kirby/src/Cms/FileVersion.php index 8db0aa5..8eb47b1 100644 --- a/kirby/src/Cms/FileVersion.php +++ b/kirby/src/Cms/FileVersion.php @@ -15,130 +15,130 @@ use Kirby\Filesystem\IsFile; */ class FileVersion { - use IsFile; + use IsFile; - protected $modifications; - protected $original; + protected $modifications; + protected $original; - /** - * Proxy for public properties, asset methods - * and content field getters - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } + /** + * Proxy for public properties, asset methods + * and content field getters + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } - // asset method proxy - if (method_exists($this->asset(), $method)) { - if ($this->exists() === false) { - $this->save(); - } + // asset method proxy + if (method_exists($this->asset(), $method)) { + if ($this->exists() === false) { + $this->save(); + } - return $this->asset()->$method(...$arguments); - } + return $this->asset()->$method(...$arguments); + } - // content fields - if (is_a($this->original(), 'Kirby\Cms\File') === true) { - return $this->original()->content()->get($method, $arguments); - } - } + // content fields + if (is_a($this->original(), 'Kirby\Cms\File') === true) { + return $this->original()->content()->get($method, $arguments); + } + } - /** - * Returns the unique ID - * - * @return string - */ - public function id(): string - { - return dirname($this->original()->id()) . '/' . $this->filename(); - } + /** + * Returns the unique ID + * + * @return string + */ + public function id(): string + { + return dirname($this->original()->id()) . '/' . $this->filename(); + } - /** - * Returns the parent Kirby App instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return $this->original()->kirby(); - } + /** + * Returns the parent Kirby App instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->original()->kirby(); + } - /** - * Returns an array with all applied modifications - * - * @return array - */ - public function modifications(): array - { - return $this->modifications ?? []; - } + /** + * Returns an array with all applied modifications + * + * @return array + */ + public function modifications(): array + { + return $this->modifications ?? []; + } - /** - * Returns the instance of the original File object - * - * @return mixed - */ - public function original() - { - return $this->original; - } + /** + * Returns the instance of the original File object + * + * @return mixed + */ + public function original() + { + return $this->original; + } - /** - * Applies the stored modifications and - * saves the file on disk - * - * @return $this - */ - public function save() - { - $this->kirby()->thumb( - $this->original()->root(), - $this->root(), - $this->modifications() - ); - return $this; - } + /** + * Applies the stored modifications and + * saves the file on disk + * + * @return $this + */ + public function save() + { + $this->kirby()->thumb( + $this->original()->root(), + $this->root(), + $this->modifications() + ); + return $this; + } - /** - * Setter for modifications - * - * @param array|null $modifications - */ - protected function setModifications(array $modifications = null) - { - $this->modifications = $modifications; - } + /** + * Setter for modifications + * + * @param array|null $modifications + */ + protected function setModifications(array $modifications = null) + { + $this->modifications = $modifications; + } - /** - * Setter for the original File object - * - * @param $original - */ - protected function setOriginal($original) - { - $this->original = $original; - } + /** + * Setter for the original File object + * + * @param $original + */ + protected function setOriginal($original) + { + $this->original = $original; + } - /** - * Converts the object to an array - * - * @return array - */ - public function toArray(): array - { - $array = array_merge($this->asset()->toArray(), [ - 'modifications' => $this->modifications(), - ]); + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge($this->asset()->toArray(), [ + 'modifications' => $this->modifications(), + ]); - ksort($array); + ksort($array); - return $array; - } + return $array; + } } diff --git a/kirby/src/Cms/Files.php b/kirby/src/Cms/Files.php index b71b82b..ab5b249 100644 --- a/kirby/src/Cms/Files.php +++ b/kirby/src/Cms/Files.php @@ -21,173 +21,177 @@ use Kirby\Filesystem\F; */ class Files extends Collection { - /** - * All registered files methods - * - * @var array - */ - public static $methods = []; + /** + * All registered files methods + * + * @var array + */ + public static $methods = []; - /** - * Adds a single file or - * an entire second collection to the - * current collection - * - * @param \Kirby\Cms\Files|\Kirby\Cms\File|string $object - * @return $this - * @throws \Kirby\Exception\InvalidArgumentException When no `File` or `Files` object or an ID of an existing file is passed - */ - public function add($object) - { - // add a files collection - if (is_a($object, self::class) === true) { - $this->data = array_merge($this->data, $object->data); + /** + * Adds a single file or + * an entire second collection to the + * current collection + * + * @param \Kirby\Cms\Files|\Kirby\Cms\File|string $object + * @return $this + * @throws \Kirby\Exception\InvalidArgumentException When no `File` or `Files` object or an ID of an existing file is passed + */ + public function add($object) + { + // add a files collection + if (is_a($object, self::class) === true) { + $this->data = array_merge($this->data, $object->data); - // add a file by id - } elseif (is_string($object) === true && $file = App::instance()->file($object)) { - $this->__set($file->id(), $file); + // add a file by id + } elseif (is_string($object) === true && $file = App::instance()->file($object)) { + $this->__set($file->id(), $file); - // add a file object - } elseif (is_a($object, 'Kirby\Cms\File') === true) { - $this->__set($object->id(), $object); + // add a file object + } elseif (is_a($object, 'Kirby\Cms\File') === true) { + $this->__set($object->id(), $object); - // give a useful error message on invalid input; - // silently ignore "empty" values for compatibility with existing setups - } elseif (in_array($object, [null, false, true], true) !== true) { - throw new InvalidArgumentException('You must pass a Files or File object or an ID of an existing file to the Files collection'); - } + // give a useful error message on invalid input; + // silently ignore "empty" values for compatibility with existing setups + } elseif (in_array($object, [null, false, true], true) !== true) { + throw new InvalidArgumentException('You must pass a Files or File object or an ID of an existing file to the Files collection'); + } - return $this; - } + return $this; + } - /** - * Sort all given files by the - * order in the array - * - * @param array $files List of file ids - * @param int $offset Sorting offset - * @return $this - */ - public function changeSort(array $files, int $offset = 0) - { - foreach ($files as $filename) { - if ($file = $this->get($filename)) { - $offset++; - $file->changeSort($offset); - } - } + /** + * Sort all given files by the + * order in the array + * + * @param array $files List of file ids + * @param int $offset Sorting offset + * @return $this + */ + public function changeSort(array $files, int $offset = 0) + { + foreach ($files as $filename) { + if ($file = $this->get($filename)) { + $offset++; + $file->changeSort($offset); + } + } - return $this; - } + return $this; + } - /** - * Creates a files collection from an array of props - * - * @param array $files - * @param \Kirby\Cms\Model $parent - * @return static - */ - public static function factory(array $files, Model $parent) - { - $collection = new static([], $parent); - $kirby = $parent->kirby(); + /** + * Creates a files collection from an array of props + * + * @param array $files + * @param \Kirby\Cms\Model $parent + * @return static + */ + public static function factory(array $files, Model $parent) + { + $collection = new static([], $parent); + $kirby = $parent->kirby(); - foreach ($files as $props) { - $props['collection'] = $collection; - $props['kirby'] = $kirby; - $props['parent'] = $parent; + foreach ($files as $props) { + $props['collection'] = $collection; + $props['kirby'] = $kirby; + $props['parent'] = $parent; - $file = File::factory($props); + $file = File::factory($props); - $collection->data[$file->id()] = $file; - } + $collection->data[$file->id()] = $file; + } - return $collection; - } + return $collection; + } - /** - * Tries to find a file by id/filename - * - * @param string $id - * @return \Kirby\Cms\File|null - */ - public function findById(string $id) - { - return $this->get(ltrim($this->parent->id() . '/' . $id, '/')); - } + /** + * Tries to find a file by id/filename + * @deprecated 3.7.0 Use `$files->find()` instead + * @todo 3.8.0 Remove method + * @codeCoverageIgnore + * + * @param string $id + * @return \Kirby\Cms\File|null + */ + public function findById(string $id) + { + Helpers::deprecated('Cms\Files::findById() has been deprecated and will be removed in Kirby 3.8.0. Use $files->find() instead.'); - /** - * Alias for FilesFinder::findById() which is - * used internally in the Files collection to - * map the get method correctly. - * - * @param string $key - * @return \Kirby\Cms\File|null - */ - public function findByKey(string $key) - { - return $this->findById($key); - } + return $this->findByKey($id); + } - /** - * Returns the file size for all - * files in the collection in a - * human-readable format - * @since 3.6.0 - * - * @param string|null|false $locale Locale for number formatting, - * `null` for the current locale, - * `false` to disable number formatting - * @return string - */ - public function niceSize($locale = null): string - { - return F::niceSize($this->size(), $locale); - } + /** + * Finds a file by its filename + * @internal Use `$files->find()` instead + * + * @param string $key + * @return \Kirby\Cms\File|null + */ + public function findByKey(string $key) + { + return $this->get(ltrim($this->parent->id() . '/' . $key, '/')); + } - /** - * Returns the raw size for all - * files in the collection - * @since 3.6.0 - * - * @return int - */ - public function size(): int - { - return F::size($this->values(fn ($file) => $file->root())); - } + /** + * Returns the file size for all + * files in the collection in a + * human-readable format + * @since 3.6.0 + * + * @param string|null|false $locale Locale for number formatting, + * `null` for the current locale, + * `false` to disable number formatting + * @return string + */ + public function niceSize($locale = null): string + { + return F::niceSize($this->size(), $locale); + } - /** - * Returns the collection sorted by - * the sort number and the filename - * - * @return static - */ - public function sorted() - { - return $this->sort('sort', 'asc', 'filename', 'asc'); - } + /** + * Returns the raw size for all + * files in the collection + * @since 3.6.0 + * + * @return int + */ + public function size(): int + { + return F::size($this->values(fn ($file) => $file->root())); + } - /** - * Filter all files by the given template - * - * @param null|string|array $template - * @return $this|static - */ - public function template($template) - { - if (empty($template) === true) { - return $this; - } + /** + * Returns the collection sorted by + * the sort number and the filename + * + * @return static + */ + public function sorted() + { + return $this->sort('sort', 'asc', 'filename', 'asc'); + } - if ($template === 'default') { - $template = ['default', '']; - } + /** + * Filter all files by the given template + * + * @param null|string|array $template + * @return $this|static + */ + public function template($template) + { + if (empty($template) === true) { + return $this; + } - return $this->filter( - 'template', - is_array($template) ? 'in' : '==', - $template - ); - } + if ($template === 'default') { + $template = ['default', '']; + } + + return $this->filter( + 'template', + is_array($template) ? 'in' : '==', + $template + ); + } } diff --git a/kirby/src/Cms/Find.php b/kirby/src/Cms/Find.php index 585f9c7..581d3ab 100644 --- a/kirby/src/Cms/Find.php +++ b/kirby/src/Cms/Find.php @@ -19,173 +19,173 @@ use Kirby\Toolkit\Str; */ class Find { - /** - * Returns the file object for the given - * parent path and filename - * - * @param string|null $path Path to file's parent model - * @param string $filename Filename - * @return \Kirby\Cms\File|null - * @throws \Kirby\Exception\NotFoundException if the file cannot be found - */ - public static function file(string $path = null, string $filename) - { - $filename = urldecode($filename); - $file = static::parent($path)->file($filename); + /** + * Returns the file object for the given + * parent path and filename + * + * @param string|null $path Path to file's parent model + * @param string $filename Filename + * @return \Kirby\Cms\File|null + * @throws \Kirby\Exception\NotFoundException if the file cannot be found + */ + public static function file(string $path = null, string $filename) + { + $filename = urldecode($filename); + $file = static::parent($path)->file($filename); - if ($file && $file->isReadable() === true) { - return $file; - } + if ($file && $file->isReadable() === true) { + return $file; + } - throw new NotFoundException([ - 'key' => 'file.notFound', - 'data' => [ - 'filename' => $filename - ] - ]); - } + throw new NotFoundException([ + 'key' => 'file.notFound', + 'data' => [ + 'filename' => $filename + ] + ]); + } - /** - * Returns the language object for the given code - * - * @param string $code Language code - * @return \Kirby\Cms\Language|null - * @throws \Kirby\Exception\NotFoundException if the language cannot be found - */ - public static function language(string $code) - { - if ($language = App::instance()->language($code)) { - return $language; - } + /** + * Returns the language object for the given code + * + * @param string $code Language code + * @return \Kirby\Cms\Language|null + * @throws \Kirby\Exception\NotFoundException if the language cannot be found + */ + public static function language(string $code) + { + if ($language = App::instance()->language($code)) { + return $language; + } - throw new NotFoundException([ - 'key' => 'language.notFound', - 'data' => [ - 'code' => $code - ] - ]); - } + throw new NotFoundException([ + 'key' => 'language.notFound', + 'data' => [ + 'code' => $code + ] + ]); + } - /** - * Returns the page object for the given id - * - * @param string $id Page's id - * @return \Kirby\Cms\Page|null - * @throws \Kirby\Exception\NotFoundException if the page cannot be found - */ - public static function page(string $id) - { - $id = str_replace(['+', ' '], '/', $id); - $page = App::instance()->page($id); + /** + * Returns the page object for the given id + * + * @param string $id Page's id + * @return \Kirby\Cms\Page|null + * @throws \Kirby\Exception\NotFoundException if the page cannot be found + */ + public static function page(string $id) + { + $id = str_replace(['+', ' '], '/', $id); + $page = App::instance()->page($id); - if ($page && $page->isReadable() === true) { - return $page; - } + if ($page && $page->isReadable() === true) { + return $page; + } - throw new NotFoundException([ - 'key' => 'page.notFound', - 'data' => [ - 'slug' => $id - ] - ]); - } + throw new NotFoundException([ + 'key' => 'page.notFound', + 'data' => [ + 'slug' => $id + ] + ]); + } - /** - * Returns the model's object for the given path - * - * @param string $path Path to parent model - * @return \Kirby\Cms\Model|null - * @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid - * @throws \Kirby\Exception\NotFoundException if the model cannot be found - */ - public static function parent(string $path) - { - $path = trim($path, '/'); - $modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/'); - $modelTypes = [ - 'site' => 'site', - 'users' => 'user', - 'pages' => 'page', - 'account' => 'account' - ]; + /** + * Returns the model's object for the given path + * + * @param string $path Path to parent model + * @return \Kirby\Cms\Model|null + * @throws \Kirby\Exception\InvalidArgumentException if the model type is invalid + * @throws \Kirby\Exception\NotFoundException if the model cannot be found + */ + public static function parent(string $path) + { + $path = trim($path, '/'); + $modelType = in_array($path, ['site', 'account']) ? $path : trim(dirname($path), '/'); + $modelTypes = [ + 'site' => 'site', + 'users' => 'user', + 'pages' => 'page', + 'account' => 'account' + ]; - $modelName = $modelTypes[$modelType] ?? null; + $modelName = $modelTypes[$modelType] ?? null; - if (Str::endsWith($modelType, '/files') === true) { - $modelName = 'file'; - } + if (Str::endsWith($modelType, '/files') === true) { + $modelName = 'file'; + } - $kirby = App::instance(); + $kirby = App::instance(); - switch ($modelName) { - case 'site': - $model = $kirby->site(); - break; - case 'account': - $model = static::user(); - break; - case 'page': - $model = static::page(basename($path)); - break; - case 'file': - $model = static::file(...explode('/files/', $path)); - break; - case 'user': - $model = $kirby->user(basename($path)); - break; - default: - throw new InvalidArgumentException('Invalid model type: ' . $modelType); - } + switch ($modelName) { + case 'site': + $model = $kirby->site(); + break; + case 'account': + $model = static::user(); + break; + case 'page': + $model = static::page(basename($path)); + break; + case 'file': + $model = static::file(...explode('/files/', $path)); + break; + case 'user': + $model = $kirby->user(basename($path)); + break; + default: + throw new InvalidArgumentException('Invalid model type: ' . $modelType); + } - if ($model) { - return $model; - } + if ($model) { + return $model; + } - throw new NotFoundException([ - 'key' => $modelName . '.undefined' - ]); - } + throw new NotFoundException([ + 'key' => $modelName . '.undefined' + ]); + } - /** - * Returns the user object for the given id or - * returns the current authenticated user if no - * id is passed - * - * @param string|null $id User's id - * @return \Kirby\Cms\User|null - * @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found - */ - public static function user(string $id = null) - { - // account is a reserved word to find the current - // user. It's used in various API and area routes. - if ($id === 'account') { - $id = null; - } + /** + * Returns the user object for the given id or + * returns the current authenticated user if no + * id is passed + * + * @param string|null $id User's id + * @return \Kirby\Cms\User|null + * @throws \Kirby\Exception\NotFoundException if the user for the given id cannot be found + */ + public static function user(string $id = null) + { + // account is a reserved word to find the current + // user. It's used in various API and area routes. + if ($id === 'account') { + $id = null; + } - $kirby = App::instance(); + $kirby = App::instance(); - // get the authenticated user - if ($id === null) { - if ($user = $kirby->user(null, $kirby->option('api.allowImpersonation', false))) { - return $user; - } + // get the authenticated user + if ($id === null) { + if ($user = $kirby->user(null, $kirby->option('api.allowImpersonation', false))) { + return $user; + } - throw new NotFoundException([ - 'key' => 'user.undefined' - ]); - } + throw new NotFoundException([ + 'key' => 'user.undefined' + ]); + } - // get a specific user by id - if ($user = $kirby->user($id)) { - return $user; - } + // get a specific user by id + if ($user = $kirby->user($id)) { + return $user; + } - throw new NotFoundException([ - 'key' => 'user.notFound', - 'data' => [ - 'name' => $id - ] - ]); - } + throw new NotFoundException([ + 'key' => 'user.notFound', + 'data' => [ + 'name' => $id + ] + ]); + } } diff --git a/kirby/src/Cms/HasChildren.php b/kirby/src/Cms/HasChildren.php index bb5be29..cb2a75e 100644 --- a/kirby/src/Cms/HasChildren.php +++ b/kirby/src/Cms/HasChildren.php @@ -16,227 +16,239 @@ use Kirby\Toolkit\Str; */ trait HasChildren { - /** - * The list of available published children - * - * @var \Kirby\Cms\Pages - */ - public $children; + /** + * The list of available published children + * + * @var \Kirby\Cms\Pages|null + */ + public $children; - /** - * The list of available draft children - * - * @var \Kirby\Cms\Pages - */ - public $drafts; + /** + * The list of available draft children + * + * @var \Kirby\Cms\Pages|null + */ + public $drafts; - /** - * Returns all published children - * - * @return \Kirby\Cms\Pages - */ - public function children() - { - if (is_a($this->children, 'Kirby\Cms\Pages') === true) { - return $this->children; - } + /** + * The combined list of available published + * and draft children + * + * @var \Kirby\Cms\Pages|null + */ + public $childrenAndDrafts; - return $this->children = Pages::factory($this->inventory()['children'], $this); - } + /** + * Returns all published children + * + * @return \Kirby\Cms\Pages + */ + public function children() + { + if (is_a($this->children, 'Kirby\Cms\Pages') === true) { + return $this->children; + } - /** - * Returns all published and draft children at the same time - * - * @return \Kirby\Cms\Pages - */ - public function childrenAndDrafts() - { - return $this->children()->merge($this->drafts()); - } + return $this->children = Pages::factory($this->inventory()['children'], $this); + } - /** - * Returns a list of IDs for the model's - * `toArray` method - * - * @return array - */ - protected function convertChildrenToArray(): array - { - return $this->children()->keys(); - } + /** + * Returns all published and draft children at the same time + * + * @return \Kirby\Cms\Pages + */ + public function childrenAndDrafts() + { + if (is_a($this->childrenAndDrafts, 'Kirby\Cms\Pages') === true) { + return $this->childrenAndDrafts; + } - /** - * Searches for a draft child by ID - * - * @param string $path - * @return \Kirby\Cms\Page|null - */ - public function draft(string $path) - { - $path = str_replace('_drafts/', '', $path); + return $this->childrenAndDrafts = $this->children()->merge($this->drafts()); + } - if (Str::contains($path, '/') === false) { - return $this->drafts()->find($path); - } + /** + * Returns a list of IDs for the model's + * `toArray` method + * + * @return array + */ + protected function convertChildrenToArray(): array + { + return $this->children()->keys(); + } - $parts = explode('/', $path); - $parent = $this; + /** + * Searches for a draft child by ID + * + * @param string $path + * @return \Kirby\Cms\Page|null + */ + public function draft(string $path) + { + $path = str_replace('_drafts/', '', $path); - foreach ($parts as $slug) { - if ($page = $parent->find($slug)) { - $parent = $page; - continue; - } + if (Str::contains($path, '/') === false) { + return $this->drafts()->find($path); + } - if ($draft = $parent->drafts()->find($slug)) { - $parent = $draft; - continue; - } + $parts = explode('/', $path); + $parent = $this; - return null; - } + foreach ($parts as $slug) { + if ($page = $parent->find($slug)) { + $parent = $page; + continue; + } - return $parent; - } + if ($draft = $parent->drafts()->find($slug)) { + $parent = $draft; + continue; + } - /** - * Returns all draft children - * - * @return \Kirby\Cms\Pages - */ - public function drafts() - { - if (is_a($this->drafts, 'Kirby\Cms\Pages') === true) { - return $this->drafts; - } + return null; + } - $kirby = $this->kirby(); + return $parent; + } - // create the inventory for all drafts - $inventory = Dir::inventory( - $this->root() . '/_drafts', - $kirby->contentExtension(), - $kirby->contentIgnore(), - $kirby->multilang() - ); + /** + * Returns all draft children + * + * @return \Kirby\Cms\Pages + */ + public function drafts() + { + if (is_a($this->drafts, 'Kirby\Cms\Pages') === true) { + return $this->drafts; + } - return $this->drafts = Pages::factory($inventory['children'], $this, true); - } + $kirby = $this->kirby(); - /** - * Finds one or multiple published children by ID - * - * @param string ...$arguments - * @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null - */ - public function find(...$arguments) - { - return $this->children()->find(...$arguments); - } + // create the inventory for all drafts + $inventory = Dir::inventory( + $this->root() . '/_drafts', + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); - /** - * Finds a single published or draft child - * - * @param string $path - * @return \Kirby\Cms\Page|null - */ - public function findPageOrDraft(string $path) - { - return $this->children()->find($path) ?? $this->drafts()->find($path); - } + return $this->drafts = Pages::factory($inventory['children'], $this, true); + } - /** - * Returns a collection of all published children of published children - * - * @return \Kirby\Cms\Pages - */ - public function grandChildren() - { - return $this->children()->children(); - } + /** + * Finds one or multiple published children by ID + * + * @param string ...$arguments + * @return \Kirby\Cms\Page|\Kirby\Cms\Pages|null + */ + public function find(...$arguments) + { + return $this->children()->find(...$arguments); + } - /** - * Checks if the model has any published children - * - * @return bool - */ - public function hasChildren(): bool - { - return $this->children()->count() > 0; - } + /** + * Finds a single published or draft child + * + * @param string $path + * @return \Kirby\Cms\Page|null + */ + public function findPageOrDraft(string $path) + { + return $this->children()->find($path) ?? $this->drafts()->find($path); + } - /** - * Checks if the model has any draft children - * - * @return bool - */ - public function hasDrafts(): bool - { - return $this->drafts()->count() > 0; - } + /** + * Returns a collection of all published children of published children + * + * @return \Kirby\Cms\Pages + */ + public function grandChildren() + { + return $this->children()->children(); + } - /** - * Checks if the page has any listed children - * - * @return bool - */ - public function hasListedChildren(): bool - { - return $this->children()->listed()->count() > 0; - } + /** + * Checks if the model has any published children + * + * @return bool + */ + public function hasChildren(): bool + { + return $this->children()->count() > 0; + } - /** - * Checks if the page has any unlisted children - * - * @return bool - */ - public function hasUnlistedChildren(): bool - { - return $this->children()->unlisted()->count() > 0; - } + /** + * Checks if the model has any draft children + * + * @return bool + */ + public function hasDrafts(): bool + { + return $this->drafts()->count() > 0; + } - /** - * Creates a flat child index - * - * @param bool $drafts If set to `true`, draft children are included - * @return \Kirby\Cms\Pages - */ - public function index(bool $drafts = false) - { - if ($drafts === true) { - return $this->childrenAndDrafts()->index($drafts); - } else { - return $this->children()->index(); - } - } + /** + * Checks if the page has any listed children + * + * @return bool + */ + public function hasListedChildren(): bool + { + return $this->children()->listed()->count() > 0; + } - /** - * Sets the published children collection - * - * @param array|null $children - * @return $this - */ - protected function setChildren(array $children = null) - { - if ($children !== null) { - $this->children = Pages::factory($children, $this); - } + /** + * Checks if the page has any unlisted children + * + * @return bool + */ + public function hasUnlistedChildren(): bool + { + return $this->children()->unlisted()->count() > 0; + } - return $this; - } + /** + * Creates a flat child index + * + * @param bool $drafts If set to `true`, draft children are included + * @return \Kirby\Cms\Pages + */ + public function index(bool $drafts = false) + { + if ($drafts === true) { + return $this->childrenAndDrafts()->index($drafts); + } else { + return $this->children()->index(); + } + } - /** - * Sets the draft children collection - * - * @param array|null $drafts - * @return $this - */ - protected function setDrafts(array $drafts = null) - { - if ($drafts !== null) { - $this->drafts = Pages::factory($drafts, $this, true); - } + /** + * Sets the published children collection + * + * @param array|null $children + * @return $this + */ + protected function setChildren(array $children = null) + { + if ($children !== null) { + $this->children = Pages::factory($children, $this); + } - return $this; - } + return $this; + } + + /** + * Sets the draft children collection + * + * @param array|null $drafts + * @return $this + */ + protected function setDrafts(array $drafts = null) + { + if ($drafts !== null) { + $this->drafts = Pages::factory($drafts, $this, true); + } + + return $this; + } } diff --git a/kirby/src/Cms/HasFiles.php b/kirby/src/Cms/HasFiles.php index 54dd797..cb4102d 100644 --- a/kirby/src/Cms/HasFiles.php +++ b/kirby/src/Cms/HasFiles.php @@ -13,214 +13,214 @@ namespace Kirby\Cms; */ trait HasFiles { - /** - * The Files collection - * - * @var \Kirby\Cms\Files - */ - protected $files; + /** + * The Files collection + * + * @var \Kirby\Cms\Files + */ + protected $files; - /** - * Filters the Files collection by type audio - * - * @return \Kirby\Cms\Files - */ - public function audio() - { - return $this->files()->filter('type', '==', 'audio'); - } + /** + * Filters the Files collection by type audio + * + * @return \Kirby\Cms\Files + */ + public function audio() + { + return $this->files()->filter('type', '==', 'audio'); + } - /** - * Filters the Files collection by type code - * - * @return \Kirby\Cms\Files - */ - public function code() - { - return $this->files()->filter('type', '==', 'code'); - } + /** + * Filters the Files collection by type code + * + * @return \Kirby\Cms\Files + */ + public function code() + { + return $this->files()->filter('type', '==', 'code'); + } - /** - * Returns a list of file ids - * for the toArray method of the model - * - * @return array - */ - protected function convertFilesToArray(): array - { - return $this->files()->keys(); - } + /** + * Returns a list of file ids + * for the toArray method of the model + * + * @return array + */ + protected function convertFilesToArray(): array + { + return $this->files()->keys(); + } - /** - * Creates a new file - * - * @param array $props - * @return \Kirby\Cms\File - */ - public function createFile(array $props) - { - $props = array_merge($props, [ - 'parent' => $this, - 'url' => null - ]); + /** + * Creates a new file + * + * @param array $props + * @return \Kirby\Cms\File + */ + public function createFile(array $props) + { + $props = array_merge($props, [ + 'parent' => $this, + 'url' => null + ]); - return File::create($props); - } + return File::create($props); + } - /** - * Filters the Files collection by type documents - * - * @return \Kirby\Cms\Files - */ - public function documents() - { - return $this->files()->filter('type', '==', 'document'); - } + /** + * Filters the Files collection by type documents + * + * @return \Kirby\Cms\Files + */ + public function documents() + { + return $this->files()->filter('type', '==', 'document'); + } - /** - * Returns a specific file by filename or the first one - * - * @param string|null $filename - * @param string $in - * @return \Kirby\Cms\File|null - */ - public function file(string $filename = null, string $in = 'files') - { - if ($filename === null) { - return $this->$in()->first(); - } + /** + * Returns a specific file by filename or the first one + * + * @param string|null $filename + * @param string $in + * @return \Kirby\Cms\File|null + */ + public function file(string $filename = null, string $in = 'files') + { + if ($filename === null) { + return $this->$in()->first(); + } - if (strpos($filename, '/') !== false) { - $path = dirname($filename); - $filename = basename($filename); + if (strpos($filename, '/') !== false) { + $path = dirname($filename); + $filename = basename($filename); - if ($page = $this->find($path)) { - return $page->$in()->find($filename); - } + if ($page = $this->find($path)) { + return $page->$in()->find($filename); + } - return null; - } + return null; + } - return $this->$in()->find($filename); - } + return $this->$in()->find($filename); + } - /** - * Returns the Files collection - * - * @return \Kirby\Cms\Files - */ - public function files() - { - if (is_a($this->files, 'Kirby\Cms\Files') === true) { - return $this->files; - } + /** + * Returns the Files collection + * + * @return \Kirby\Cms\Files + */ + public function files() + { + if (is_a($this->files, 'Kirby\Cms\Files') === true) { + return $this->files; + } - return $this->files = Files::factory($this->inventory()['files'], $this); - } + return $this->files = Files::factory($this->inventory()['files'], $this); + } - /** - * Checks if the Files collection has any audio files - * - * @return bool - */ - public function hasAudio(): bool - { - return $this->audio()->count() > 0; - } + /** + * Checks if the Files collection has any audio files + * + * @return bool + */ + public function hasAudio(): bool + { + return $this->audio()->count() > 0; + } - /** - * Checks if the Files collection has any code files - * - * @return bool - */ - public function hasCode(): bool - { - return $this->code()->count() > 0; - } + /** + * Checks if the Files collection has any code files + * + * @return bool + */ + public function hasCode(): bool + { + return $this->code()->count() > 0; + } - /** - * Checks if the Files collection has any document files - * - * @return bool - */ - public function hasDocuments(): bool - { - return $this->documents()->count() > 0; - } + /** + * Checks if the Files collection has any document files + * + * @return bool + */ + public function hasDocuments(): bool + { + return $this->documents()->count() > 0; + } - /** - * Checks if the Files collection has any files - * - * @return bool - */ - public function hasFiles(): bool - { - return $this->files()->count() > 0; - } + /** + * Checks if the Files collection has any files + * + * @return bool + */ + public function hasFiles(): bool + { + return $this->files()->count() > 0; + } - /** - * Checks if the Files collection has any images - * - * @return bool - */ - public function hasImages(): bool - { - return $this->images()->count() > 0; - } + /** + * Checks if the Files collection has any images + * + * @return bool + */ + public function hasImages(): bool + { + return $this->images()->count() > 0; + } - /** - * Checks if the Files collection has any videos - * - * @return bool - */ - public function hasVideos(): bool - { - return $this->videos()->count() > 0; - } + /** + * Checks if the Files collection has any videos + * + * @return bool + */ + public function hasVideos(): bool + { + return $this->videos()->count() > 0; + } - /** - * Returns a specific image by filename or the first one - * - * @param string|null $filename - * @return \Kirby\Cms\File|null - */ - public function image(string $filename = null) - { - return $this->file($filename, 'images'); - } + /** + * Returns a specific image by filename or the first one + * + * @param string|null $filename + * @return \Kirby\Cms\File|null + */ + public function image(string $filename = null) + { + return $this->file($filename, 'images'); + } - /** - * Filters the Files collection by type image - * - * @return \Kirby\Cms\Files - */ - public function images() - { - return $this->files()->filter('type', '==', 'image'); - } + /** + * Filters the Files collection by type image + * + * @return \Kirby\Cms\Files + */ + public function images() + { + return $this->files()->filter('type', '==', 'image'); + } - /** - * Sets the Files collection - * - * @param \Kirby\Cms\Files|null $files - * @return $this - */ - protected function setFiles(array $files = null) - { - if ($files !== null) { - $this->files = Files::factory($files, $this); - } + /** + * Sets the Files collection + * + * @param \Kirby\Cms\Files|null $files + * @return $this + */ + protected function setFiles(array $files = null) + { + if ($files !== null) { + $this->files = Files::factory($files, $this); + } - return $this; - } + return $this; + } - /** - * Filters the Files collection by type videos - * - * @return \Kirby\Cms\Files - */ - public function videos() - { - return $this->files()->filter('type', '==', 'video'); - } + /** + * Filters the Files collection by type videos + * + * @return \Kirby\Cms\Files + */ + public function videos() + { + return $this->files()->filter('type', '==', 'video'); + } } diff --git a/kirby/src/Cms/HasMethods.php b/kirby/src/Cms/HasMethods.php index 36a19ae..053eb8b 100644 --- a/kirby/src/Cms/HasMethods.php +++ b/kirby/src/Cms/HasMethods.php @@ -15,66 +15,66 @@ use Kirby\Exception\BadMethodCallException; */ trait HasMethods { - /** - * All registered methods - * - * @var array - */ - public static $methods = []; + /** + * All registered methods + * + * @var array + */ + public static $methods = []; - /** - * Calls a registered method class with the - * passed arguments - * - * @internal - * @param string $method - * @param array $args - * @return mixed - * @throws \Kirby\Exception\BadMethodCallException - */ - public function callMethod(string $method, array $args = []) - { - $closure = $this->getMethod($method); + /** + * Calls a registered method class with the + * passed arguments + * + * @internal + * @param string $method + * @param array $args + * @return mixed + * @throws \Kirby\Exception\BadMethodCallException + */ + public function callMethod(string $method, array $args = []) + { + $closure = $this->getMethod($method); - if ($closure === null) { - throw new BadMethodCallException('The method ' . $method . ' does not exist'); - } + if ($closure === null) { + throw new BadMethodCallException('The method ' . $method . ' does not exist'); + } - return $closure->call($this, ...$args); - } + return $closure->call($this, ...$args); + } - /** - * Checks if the object has a registered method - * - * @internal - * @param string $method - * @return bool - */ - public function hasMethod(string $method): bool - { - return $this->getMethod($method) !== null; - } + /** + * Checks if the object has a registered method + * + * @internal + * @param string $method + * @return bool + */ + public function hasMethod(string $method): bool + { + return $this->getMethod($method) !== null; + } - /** - * Returns a registered method by name, either from - * the current class or from a parent class ordered by - * inheritance order (top to bottom) - * - * @param string $method - * @return \Closure|null - */ - protected function getMethod(string $method) - { - if (isset(static::$methods[$method]) === true) { - return static::$methods[$method]; - } + /** + * Returns a registered method by name, either from + * the current class or from a parent class ordered by + * inheritance order (top to bottom) + * + * @param string $method + * @return \Closure|null + */ + protected function getMethod(string $method) + { + if (isset(static::$methods[$method]) === true) { + return static::$methods[$method]; + } - foreach (class_parents($this) as $parent) { - if (isset($parent::$methods[$method]) === true) { - return $parent::$methods[$method]; - } - } + foreach (class_parents($this) as $parent) { + if (isset($parent::$methods[$method]) === true) { + return $parent::$methods[$method]; + } + } - return null; - } + return null; + } } diff --git a/kirby/src/Cms/HasSiblings.php b/kirby/src/Cms/HasSiblings.php index b4f1846..ad6fa7d 100644 --- a/kirby/src/Cms/HasSiblings.php +++ b/kirby/src/Cms/HasSiblings.php @@ -14,169 +14,169 @@ namespace Kirby\Cms; */ trait HasSiblings { - /** - * Returns the position / index in the collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return int - */ - public function indexOf($collection = null): int - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Returns the position / index in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return int + */ + public function indexOf($collection = null): int + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->indexOf($this); - } + return $collection->indexOf($this); + } - /** - * Returns the next item in the collection if available - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Model|null - */ - public function next($collection = null) - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Returns the next item in the collection if available + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Model|null + */ + public function next($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->nth($this->indexOf($collection) + 1); - } + return $collection->nth($this->indexOf($collection) + 1); + } - /** - * Returns the end of the collection starting after the current item - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Collection - */ - public function nextAll($collection = null) - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Returns the end of the collection starting after the current item + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Collection + */ + public function nextAll($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->slice($this->indexOf($collection) + 1); - } + return $collection->slice($this->indexOf($collection) + 1); + } - /** - * Returns the previous item in the collection if available - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Model|null - */ - public function prev($collection = null) - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Returns the previous item in the collection if available + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Model|null + */ + public function prev($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->nth($this->indexOf($collection) - 1); - } + return $collection->nth($this->indexOf($collection) - 1); + } - /** - * Returns the beginning of the collection before the current item - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Collection - */ - public function prevAll($collection = null) - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Returns the beginning of the collection before the current item + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Collection + */ + public function prevAll($collection = null) + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->slice(0, $this->indexOf($collection)); - } + return $collection->slice(0, $this->indexOf($collection)); + } - /** - * Returns all sibling elements - * - * @param bool $self - * @return \Kirby\Cms\Collection - */ - public function siblings(bool $self = true) - { - $siblings = $this->siblingsCollection(); + /** + * Returns all sibling elements + * + * @param bool $self + * @return \Kirby\Cms\Collection + */ + public function siblings(bool $self = true) + { + $siblings = $this->siblingsCollection(); - if ($self === false) { - return $siblings->not($this); - } + if ($self === false) { + return $siblings->not($this); + } - return $siblings; - } + return $siblings; + } - /** - * Checks if there's a next item in the collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function hasNext($collection = null): bool - { - return $this->next($collection) !== null; - } + /** + * Checks if there's a next item in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasNext($collection = null): bool + { + return $this->next($collection) !== null; + } - /** - * Checks if there's a previous item in the collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function hasPrev($collection = null): bool - { - return $this->prev($collection) !== null; - } + /** + * Checks if there's a previous item in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasPrev($collection = null): bool + { + return $this->prev($collection) !== null; + } - /** - * Checks if the item is the first in the collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function isFirst($collection = null): bool - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Checks if the item is the first in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function isFirst($collection = null): bool + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->first()->is($this); - } + return $collection->first()->is($this); + } - /** - * Checks if the item is the last in the collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function isLast($collection = null): bool - { - if ($collection === null) { - $collection = $this->siblingsCollection(); - } + /** + * Checks if the item is the last in the collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function isLast($collection = null): bool + { + if ($collection === null) { + $collection = $this->siblingsCollection(); + } - return $collection->last()->is($this); - } + return $collection->last()->is($this); + } - /** - * Checks if the item is at a certain position - * - * @param \Kirby\Cms\Collection|null $collection - * @param int $n - * - * @return bool - */ - public function isNth(int $n, $collection = null): bool - { - return $this->indexOf($collection) === $n; - } + /** + * Checks if the item is at a certain position + * + * @param \Kirby\Cms\Collection|null $collection + * @param int $n + * + * @return bool + */ + public function isNth(int $n, $collection = null): bool + { + return $this->indexOf($collection) === $n; + } } diff --git a/kirby/src/Cms/Helpers.php b/kirby/src/Cms/Helpers.php new file mode 100644 index 0000000..a7c92fa --- /dev/null +++ b/kirby/src/Cms/Helpers.php @@ -0,0 +1,128 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://getkirby.com/license + */ +class Helpers +{ + /** + * Triggers a deprecation warning if debug mode is active + * + * @param string $message + * @return bool Whether the warning was triggered + */ + public static function deprecated(string $message): bool + { + if (App::instance()->option('debug') === true) { + return trigger_error($message, E_USER_DEPRECATED) === true; + } + + return false; + } + + /** + * Simple object and variable dumper + * to help with debugging. + * + * @param mixed $variable + * @param bool $echo + * @return string + */ + public static function dump($variable, bool $echo = true): string + { + $kirby = App::instance(); + return ($kirby->component('dump'))($kirby, $variable, $echo); + } + + /** + * Performs an action with custom handling + * for all PHP errors and warnings + * @since 3.7.4 + * + * @param \Closure $action Any action that may cause an error or warning + * @param \Closure $handler Custom callback like for `set_error_handler()`; + * the first argument is a return value override passed + * by reference, the additional arguments come from + * `set_error_handler()`; returning `false` activates + * error handling by Whoops and/or PHP + * @return mixed Return value of the `$action` closure, possibly overridden by `$handler` + */ + public static function handleErrors(Closure $action, Closure $handler) + { + $override = $oldHandler = null; + $oldHandler = set_error_handler(function () use (&$override, &$oldHandler, $handler) { + $handlerResult = $handler($override, ...func_get_args()); + + if ($handlerResult === false) { + // handle other warnings with Whoops if loaded + if (is_callable($oldHandler) === true) { + return $oldHandler(...func_get_args()); + } + + // otherwise use the standard error handler + return false; // @codeCoverageIgnore + } + + // no additional error handling + return true; + }); + + $result = $action(); + + restore_error_handler(); + + return $override ?? $result; + } + + /** + * Checks if a helper was overridden by the user + * by setting the `KIRBY_HELPER_*` constant + * @internal + * + * @param string $name Name of the helper + * @return bool + */ + public static function hasOverride(string $name): bool + { + $name = 'KIRBY_HELPER_' . strtoupper($name); + return defined($name) === true && constant($name) === false; + } + + /** + * Determines the size/length of numbers, + * strings, arrays and countable objects + * + * @param mixed $value + * @return int + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function size($value): int + { + if (is_numeric($value)) { + return (int)$value; + } + + if (is_string($value)) { + return Str::length(trim($value)); + } + + if (is_countable($value)) { + return count($value); + } + + throw new InvalidArgumentException('Could not determine the size of the given value'); + } +} diff --git a/kirby/src/Cms/Html.php b/kirby/src/Cms/Html.php index 37c470c..1af6087 100644 --- a/kirby/src/Cms/Html.php +++ b/kirby/src/Cms/Html.php @@ -2,6 +2,9 @@ namespace Kirby\Cms; +use Kirby\Filesystem\F; +use Kirby\Toolkit\A; + /** * The `Html` class provides methods for building * common HTML tags and also contains some helper @@ -15,16 +18,132 @@ namespace Kirby\Cms; */ class Html extends \Kirby\Toolkit\Html { - /** - * Generates an `a` tag with an absolute Url - * - * @param string|null $href Relative or absolute Url - * @param string|array|null $text If `null`, the link will be used as link text. If an array is passed, each element will be added unencoded - * @param array $attr Additional attributes for the a tag. - * @return string - */ - public static function link(string $href = null, $text = null, array $attr = []): string - { - return parent::link(Url::to($href), $text, $attr); - } + /** + * Creates one or multiple CSS link tags + * @since 3.7.0 + * + * @param string|array $url Relative or absolute URLs, an array of URLs or `@auto` for automatic template css loading + * @param string|array $options Pass an array of attributes for the link tag or a media attribute string + * @return string|null + */ + public static function css($url, $options = null): ?string + { + if (is_array($url) === true) { + $links = A::map($url, fn ($url) => static::css($url, $options)); + return implode(PHP_EOL, $links); + } + + if (is_string($options) === true) { + $options = ['media' => $options]; + } + + $kirby = App::instance(); + + if ($url === '@auto') { + if (!$url = Url::toTemplateAsset('css/templates', 'css')) { + return null; + } + } + + // only valid value for 'rel' is 'alternate stylesheet', if 'title' is given as well + if ( + ($options['rel'] ?? '') !== 'alternate stylesheet' || + ($options['title'] ?? '') === '' + ) { + $options['rel'] = 'stylesheet'; + } + + $url = ($kirby->component('css'))($kirby, $url, $options); + $url = Url::to($url); + $attr = array_merge((array)$options, [ + 'href' => $url + ]); + + return ''; + } + + /** + * Generates an `a` tag with an absolute Url + * + * @param string|null $href Relative or absolute Url + * @param string|array|null $text If `null`, the link will be used as link text. If an array is passed, each element will be added unencoded + * @param array $attr Additional attributes for the a tag. + * @return string + */ + public static function link(string $href = null, $text = null, array $attr = []): string + { + return parent::link(Url::to($href), $text, $attr); + } + + /** + * Creates a script tag to load a javascript file + * @since 3.7.0 + * + * @param string|array $url + * @param string|array $options + * @return string|null + */ + public static function js($url, $options = null): ?string + { + if (is_array($url) === true) { + $scripts = A::map($url, fn ($url) => static::js($url, $options)); + return implode(PHP_EOL, $scripts); + } + + if (is_bool($options) === true) { + $options = ['async' => $options]; + } + + $kirby = App::instance(); + + if ($url === '@auto') { + if (!$url = Url::toTemplateAsset('js/templates', 'js')) { + return null; + } + } + + $url = ($kirby->component('js'))($kirby, $url, $options); + $url = Url::to($url); + $attr = array_merge((array)$options, ['src' => $url]); + + return ''; + } + + /** + * Includes an SVG file by absolute or + * relative file path. + * @since 3.7.0 + * + * @param string|\Kirby\Cms\File $file + * @return string|false + */ + public static function svg($file) + { + // support for Kirby's file objects + if ( + is_a($file, 'Kirby\Cms\File') === true && + $file->extension() === 'svg' + ) { + return $file->read(); + } + + if (is_string($file) === false) { + return false; + } + + $extension = F::extension($file); + + // check for valid svg files + if ($extension !== 'svg') { + return false; + } + + // try to convert relative paths to absolute + if (file_exists($file) === false) { + $root = App::instance()->root(); + $file = realpath($root . '/' . $file); + } + + return F::read($file); + } } diff --git a/kirby/src/Cms/Ingredients.php b/kirby/src/Cms/Ingredients.php index f0dcc44..42442b3 100644 --- a/kirby/src/Cms/Ingredients.php +++ b/kirby/src/Cms/Ingredients.php @@ -16,80 +16,80 @@ namespace Kirby\Cms; */ class Ingredients { - /** - * @var array - */ - protected $ingredients = []; + /** + * @var array + */ + protected $ingredients = []; - /** - * Creates a new ingredient collection - * - * @param array $ingredients - */ - public function __construct(array $ingredients) - { - $this->ingredients = $ingredients; - } + /** + * Creates a new ingredient collection + * + * @param array $ingredients + */ + public function __construct(array $ingredients) + { + $this->ingredients = $ingredients; + } - /** - * Magic getter for single ingredients - * - * @param string $method - * @param array|null $args - * @return mixed - */ - public function __call(string $method, array $args = null) - { - return $this->ingredients[$method] ?? null; - } + /** + * Magic getter for single ingredients + * + * @param string $method + * @param array|null $args + * @return mixed + */ + public function __call(string $method, array $args = null) + { + return $this->ingredients[$method] ?? null; + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->ingredients; - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->ingredients; + } - /** - * Get a single ingredient by key - * - * @param string $key - * @return mixed - */ - public function __get(string $key) - { - return $this->ingredients[$key] ?? null; - } + /** + * Get a single ingredient by key + * + * @param string $key + * @return mixed + */ + public function __get(string $key) + { + return $this->ingredients[$key] ?? null; + } - /** - * Resolves all ingredient callbacks - * and creates a plain array - * - * @internal - * @param array $ingredients - * @return static - */ - public static function bake(array $ingredients) - { - foreach ($ingredients as $name => $ingredient) { - if (is_a($ingredient, 'Closure') === true) { - $ingredients[$name] = $ingredient($ingredients); - } - } + /** + * Resolves all ingredient callbacks + * and creates a plain array + * + * @internal + * @param array $ingredients + * @return static + */ + public static function bake(array $ingredients) + { + foreach ($ingredients as $name => $ingredient) { + if (is_a($ingredient, 'Closure') === true) { + $ingredients[$name] = $ingredient($ingredients); + } + } - return new static($ingredients); - } + return new static($ingredients); + } - /** - * Returns all ingredients as plain array - * - * @return array - */ - public function toArray(): array - { - return $this->ingredients; - } + /** + * Returns all ingredients as plain array + * + * @return array + */ + public function toArray(): array + { + return $this->ingredients; + } } diff --git a/kirby/src/Cms/Item.php b/kirby/src/Cms/Item.php index aa1f313..2f6922e 100644 --- a/kirby/src/Cms/Item.php +++ b/kirby/src/Cms/Item.php @@ -2,6 +2,8 @@ namespace Kirby\Cms; +use Kirby\Toolkit\Str; + /** * The Item class is the foundation * for every object in context with @@ -20,118 +22,118 @@ namespace Kirby\Cms; */ class Item { - use HasSiblings; + use HasSiblings; - public const ITEMS_CLASS = '\Kirby\Cms\Items'; + public const ITEMS_CLASS = '\Kirby\Cms\Items'; - /** - * @var string - */ - protected $id; + /** + * @var string + */ + protected $id; - /** - * @var array - */ - protected $params; + /** + * @var array + */ + protected $params; - /** - * @var \Kirby\Cms\Page|\Kirby\Cms\Site|\Kirby\Cms\File|\Kirby\Cms\User - */ - protected $parent; + /** + * @var \Kirby\Cms\Page|\Kirby\Cms\Site|\Kirby\Cms\File|\Kirby\Cms\User + */ + protected $parent; - /** - * @var \Kirby\Cms\Items - */ - protected $siblings; + /** + * @var \Kirby\Cms\Items + */ + protected $siblings; - /** - * Creates a new item - * - * @param array $params - */ - public function __construct(array $params = []) - { - $siblingsClass = static::ITEMS_CLASS; + /** + * Creates a new item + * + * @param array $params + */ + public function __construct(array $params = []) + { + $siblingsClass = static::ITEMS_CLASS; - $this->id = $params['id'] ?? uuid(); - $this->params = $params; - $this->parent = $params['parent'] ?? site(); - $this->siblings = $params['siblings'] ?? new $siblingsClass(); - } + $this->id = $params['id'] ?? Str::uuid(); + $this->params = $params; + $this->parent = $params['parent'] ?? App::instance()->site(); + $this->siblings = $params['siblings'] ?? new $siblingsClass(); + } - /** - * Static Item factory - * - * @param array $params - * @return \Kirby\Cms\Item - */ - public static function factory(array $params) - { - return new static($params); - } + /** + * Static Item factory + * + * @param array $params + * @return \Kirby\Cms\Item + */ + public static function factory(array $params) + { + return new static($params); + } - /** - * Returns the unique item id (UUID v4) - * - * @return string - */ - public function id(): string - { - return $this->id; - } + /** + * Returns the unique item id (UUID v4) + * + * @return string + */ + public function id(): string + { + return $this->id; + } - /** - * Compares the item to another one - * - * @param \Kirby\Cms\Item $item - * @return bool - */ - public function is(Item $item): bool - { - return $this->id() === $item->id(); - } + /** + * Compares the item to another one + * + * @param \Kirby\Cms\Item $item + * @return bool + */ + public function is(Item $item): bool + { + return $this->id() === $item->id(); + } - /** - * Returns the Kirby instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return $this->parent()->kirby(); - } + /** + * Returns the Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->parent()->kirby(); + } - /** - * Returns the parent model - * - * @return \Kirby\Cms\Page|\Kirby\Cms\Site|\Kirby\Cms\File|\Kirby\Cms\User - */ - public function parent() - { - return $this->parent; - } + /** + * Returns the parent model + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site|\Kirby\Cms\File|\Kirby\Cms\User + */ + public function parent() + { + return $this->parent; + } - /** - * Returns the sibling collection - * This is required by the HasSiblings trait - * - * @return \Kirby\Cms\Items - * @psalm-return self::ITEMS_CLASS - */ - protected function siblingsCollection() - { - return $this->siblings; - } + /** + * Returns the sibling collection + * This is required by the HasSiblings trait + * + * @return \Kirby\Cms\Items + * @psalm-return self::ITEMS_CLASS + */ + protected function siblingsCollection() + { + return $this->siblings; + } - /** - * Converts the item to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'id' => $this->id(), - ]; - } + /** + * Converts the item to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'id' => $this->id(), + ]; + } } diff --git a/kirby/src/Cms/Items.php b/kirby/src/Cms/Items.php index 7e72272..6bbecb5 100644 --- a/kirby/src/Cms/Items.php +++ b/kirby/src/Cms/Items.php @@ -17,81 +17,81 @@ use Exception; */ class Items extends Collection { - public const ITEM_CLASS = '\Kirby\Cms\Item'; + public const ITEM_CLASS = '\Kirby\Cms\Item'; - /** - * @var array - */ - protected $options; + /** + * @var array + */ + protected $options; - /** - * @var \Kirby\Cms\ModelWithContent - */ - protected $parent; + /** + * @var \Kirby\Cms\ModelWithContent + */ + protected $parent; - /** - * Constructor - * - * @param array $objects - * @param array $options - */ - public function __construct($objects = [], array $options = []) - { - $this->options = $options; - $this->parent = $options['parent'] ?? site(); + /** + * Constructor + * + * @param array $objects + * @param array $options + */ + public function __construct($objects = [], array $options = []) + { + $this->options = $options; + $this->parent = $options['parent'] ?? App::instance()->site(); - parent::__construct($objects, $this->parent); - } + parent::__construct($objects, $this->parent); + } - /** - * Creates a new item collection from a - * an array of item props - * - * @param array $items - * @param array $params - * @return \Kirby\Cms\Items - */ - public static function factory(array $items = null, array $params = []) - { - $options = array_merge([ - 'options' => [], - 'parent' => site(), - ], $params); + /** + * Creates a new item collection from a + * an array of item props + * + * @param array $items + * @param array $params + * @return \Kirby\Cms\Items + */ + public static function factory(array $items = null, array $params = []) + { + $options = array_merge([ + 'options' => [], + 'parent' => App::instance()->site(), + ], $params); - if (empty($items) === true || is_array($items) === false) { - return new static(); - } + if (empty($items) === true || is_array($items) === false) { + return new static(); + } - if (is_array($options) === false) { - throw new Exception('Invalid item options'); - } + if (is_array($options) === false) { + throw new Exception('Invalid item options'); + } - // create a new collection of blocks - $collection = new static([], $options); + // create a new collection of blocks + $collection = new static([], $options); - foreach ($items as $params) { - if (is_array($params) === false) { - continue; - } + foreach ($items as $params) { + if (is_array($params) === false) { + continue; + } - $params['options'] = $options['options']; - $params['parent'] = $options['parent']; - $params['siblings'] = $collection; - $class = static::ITEM_CLASS; - $item = $class::factory($params); - $collection->append($item->id(), $item); - } + $params['options'] = $options['options']; + $params['parent'] = $options['parent']; + $params['siblings'] = $collection; + $class = static::ITEM_CLASS; + $item = $class::factory($params); + $collection->append($item->id(), $item); + } - return $collection; - } + return $collection; + } - /** - * Convert the items to an array - * - * @return array - */ - public function toArray(Closure $map = null): array - { - return array_values(parent::toArray($map)); - } + /** + * Convert the items to an array + * + * @return array + */ + public function toArray(Closure $map = null): array + { + return array_values(parent::toArray($map)); + } } diff --git a/kirby/src/Cms/Language.php b/kirby/src/Cms/Language.php index d15eb7c..04fec9d 100644 --- a/kirby/src/Cms/Language.php +++ b/kirby/src/Cms/Language.php @@ -28,667 +28,667 @@ use Throwable; */ class Language extends Model { - /** - * @var string - */ - protected $code; - - /** - * @var bool - */ - protected $default; - - /** - * @var string - */ - protected $direction; - - /** - * @var array - */ - protected $locale; - - /** - * @var string - */ - protected $name; - - /** - * @var array|null - */ - protected $slugs; - - /** - * @var array|null - */ - protected $smartypants; - - /** - * @var array|null - */ - protected $translations; - - /** - * @var string - */ - protected $url; - - /** - * Creates a new language object - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setRequiredProperties($props, [ - 'code' - ]); - - $this->setOptionalProperties($props, [ - 'default', - 'direction', - 'locale', - 'name', - 'slugs', - 'smartypants', - 'translations', - 'url', - ]); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } - - /** - * Returns the language code - * when the language is converted to a string - * - * @return string - */ - public function __toString(): string - { - return $this->code(); - } - - /** - * Returns the base Url for the language - * without the path or other cruft - * - * @return string - */ - public function baseUrl(): string - { - $kirbyUrl = $this->kirby()->url(); - $languageUrl = $this->url(); - - if (empty($this->url)) { - return $kirbyUrl; - } - - if (Str::startsWith($languageUrl, $kirbyUrl) === true) { - return $kirbyUrl; - } - - return Url::base($languageUrl) ?? $kirbyUrl; - } - - /** - * Returns the language code/id. - * The language code is used in - * text file names as appendix. - * - * @return string - */ - public function code(): string - { - return $this->code; - } - - /** - * Internal converter to create or remove - * translation files. - * - * @param string $from - * @param string $to - * @return bool - */ - protected static function converter(string $from, string $to): bool - { - $kirby = App::instance(); - $site = $kirby->site(); - - // convert site - foreach ($site->files() as $file) { - F::move($file->contentFile($from, true), $file->contentFile($to, true)); - } - - F::move($site->contentFile($from, true), $site->contentFile($to, true)); - - // convert all pages - foreach ($kirby->site()->index(true) as $page) { - foreach ($page->files() as $file) { - F::move($file->contentFile($from, true), $file->contentFile($to, true)); - } - - F::move($page->contentFile($from, true), $page->contentFile($to, true)); - } - - // convert all users - foreach ($kirby->users() as $user) { - foreach ($user->files() as $file) { - F::move($file->contentFile($from, true), $file->contentFile($to, true)); - } - - F::move($user->contentFile($from, true), $user->contentFile($to, true)); - } - - return true; - } - - /** - * Creates a new language object - * - * @internal - * @param array $props - * @return static - */ - public static function create(array $props) - { - $props['code'] = Str::slug($props['code'] ?? null); - $kirby = App::instance(); - $languages = $kirby->languages(); - - // make the first language the default language - if ($languages->count() === 0) { - $props['default'] = true; - } - - $language = new static($props); - - // validate the new language - LanguageRules::create($language); - - $language->save(); - - if ($languages->count() === 0) { - static::converter('', $language->code()); - } - - // update the main languages collection in the app instance - App::instance()->languages(false)->append($language->code(), $language); - - return $language; - } - - /** - * Delete the current language and - * all its translation files - * - * @internal - * @return bool - * @throws \Kirby\Exception\Exception - */ - public function delete(): bool - { - $kirby = App::instance(); - $languages = $kirby->languages(); - $code = $this->code(); - $isLast = $languages->count() === 1; - - if (F::remove($this->root()) !== true) { - throw new Exception('The language could not be deleted'); - } - - if ($isLast === true) { - $this->converter($code, ''); - } else { - $this->deleteContentFiles($code); - } - - // get the original language collection and remove the current language - $kirby->languages(false)->remove($code); - - return true; - } - - /** - * When the language is deleted, all content files with - * the language code must be removed as well. - * - * @param mixed $code - * @return bool - */ - protected function deleteContentFiles($code): bool - { - $kirby = App::instance(); - $site = $kirby->site(); - - F::remove($site->contentFile($code, true)); - - foreach ($kirby->site()->index(true) as $page) { - foreach ($page->files() as $file) { - F::remove($file->contentFile($code, true)); - } - - F::remove($page->contentFile($code, true)); - } - - foreach ($kirby->users() as $user) { - foreach ($user->files() as $file) { - F::remove($file->contentFile($code, true)); - } - - F::remove($user->contentFile($code, true)); - } - - return true; - } - - /** - * Reading direction of this language - * - * @return string - */ - public function direction(): string - { - return $this->direction; - } - - /** - * Check if the language file exists - * - * @return bool - */ - public function exists(): bool - { - return file_exists($this->root()); - } - - /** - * Checks if this is the default language - * for the site. - * - * @return bool - */ - public function isDefault(): bool - { - return $this->default; - } - - /** - * The id is required for collections - * to work properly. The code is used as id - * - * @return string - */ - public function id(): string - { - return $this->code; - } - - /** - * Loads the language rules for provided locale code - * - * @param string $code - */ - public static function loadRules(string $code) - { - $kirby = kirby(); - $code = Str::contains($code, '.') ? Str::before($code, '.') : $code; - $file = $kirby->root('i18n:rules') . '/' . $code . '.json'; - - if (F::exists($file) === false) { - $file = $kirby->root('i18n:rules') . '/' . Str::before($code, '_') . '.json'; - } - - try { - return Data::read($file); - } catch (\Exception $e) { - return []; - } - } - - /** - * Returns the PHP locale setting array - * - * @param int $category If passed, returns the locale for the specified category (e.g. LC_ALL) as string - * @return array|string - */ - public function locale(int $category = null) - { - if ($category !== null) { - return $this->locale[$category] ?? $this->locale[LC_ALL] ?? null; - } else { - return $this->locale; - } - } - - /** - * Returns the human-readable name - * of the language - * - * @return string - */ - public function name(): string - { - return $this->name; - } - - /** - * Returns the URL path for the language - * - * @return string - */ - public function path(): string - { - if ($this->url === null) { - return $this->code; - } - - return Url::path($this->url); - } - - /** - * Returns the routing pattern for the language - * - * @return string - */ - public function pattern(): string - { - $path = $this->path(); - - if (empty($path) === true) { - return '(:all)'; - } - - return $path . '/(:all?)'; - } - - /** - * Returns the absolute path to the language file - * - * @return string - */ - public function root(): string - { - return App::instance()->root('languages') . '/' . $this->code() . '.php'; - } - - /** - * Returns the LanguageRouter instance - * which is used to handle language specific - * routes. - * - * @return \Kirby\Cms\LanguageRouter - */ - public function router() - { - return new LanguageRouter($this); - } - - /** - * Get slug rules for language - * - * @internal - * @return array - */ - public function rules(): array - { - $code = $this->locale(LC_CTYPE); - $data = static::loadRules($code); - return array_merge($data, $this->slugs()); - } - - /** - * Saves the language settings in the languages folder - * - * @internal - * @return $this - */ - public function save() - { - try { - $existingData = Data::read($this->root()); - } catch (Throwable $e) { - $existingData = []; - } - - $props = [ - 'code' => $this->code(), - 'default' => $this->isDefault(), - 'direction' => $this->direction(), - 'locale' => Locale::export($this->locale()), - 'name' => $this->name(), - 'translations' => $this->translations(), - 'url' => $this->url, - ]; - - $data = array_merge($existingData, $props); - - ksort($data); - - Data::write($this->root(), $data); - - return $this; - } - - /** - * @param string $code - * @return $this - */ - protected function setCode(string $code) - { - $this->code = trim($code); - return $this; - } - - /** - * @param bool $default - * @return $this - */ - protected function setDefault(bool $default = false) - { - $this->default = $default; - return $this; - } - - /** - * @param string $direction - * @return $this - */ - protected function setDirection(string $direction = 'ltr') - { - $this->direction = $direction === 'rtl' ? 'rtl' : 'ltr'; - return $this; - } - - /** - * @param string|array $locale - * @return $this - */ - protected function setLocale($locale = null) - { - if ($locale === null) { - $this->locale = [LC_ALL => $this->code]; - } else { - $this->locale = Locale::normalize($locale); - } - - return $this; - } - - /** - * @param string $name - * @return $this - */ - protected function setName(string $name = null) - { - $this->name = trim($name ?? $this->code); - return $this; - } - - /** - * @param array $slugs - * @return $this - */ - protected function setSlugs(array $slugs = null) - { - $this->slugs = $slugs ?? []; - return $this; - } - - /** - * @param array $smartypants - * @return $this - */ - protected function setSmartypants(array $smartypants = null) - { - $this->smartypants = $smartypants ?? []; - return $this; - } - - /** - * @param array $translations - * @return $this - */ - protected function setTranslations(array $translations = null) - { - $this->translations = $translations ?? []; - return $this; - } - - /** - * @param string $url - * @return $this - */ - protected function setUrl(string $url = null) - { - $this->url = $url; - return $this; - } - - /** - * Returns the custom slug rules for this language - * - * @return array - */ - public function slugs(): array - { - return $this->slugs; - } - - /** - * Returns the custom SmartyPants options for this language - * - * @return array - */ - public function smartypants(): array - { - return $this->smartypants; - } - - /** - * Returns the most important - * properties as array - * - * @return array - */ - public function toArray(): array - { - return [ - 'code' => $this->code(), - 'default' => $this->isDefault(), - 'direction' => $this->direction(), - 'locale' => $this->locale(), - 'name' => $this->name(), - 'rules' => $this->rules(), - 'url' => $this->url() - ]; - } - - /** - * Returns the translation strings for this language - * - * @return array - */ - public function translations(): array - { - return $this->translations; - } - - /** - * Returns the absolute Url for the language - * - * @return string - */ - public function url(): string - { - $url = $this->url; - - if ($url === null) { - $url = '/' . $this->code; - } - - return Url::makeAbsolute($url, $this->kirby()->url()); - } - - /** - * Update language properties and save them - * - * @internal - * @param array $props - * @return static - */ - public function update(array $props = null) - { - // don't change the language code - unset($props['code']); - - // make sure the slug is nice and clean - $props['slug'] = Str::slug($props['slug'] ?? null); - - $kirby = App::instance(); - $updated = $this->clone($props); - - // validate the updated language - LanguageRules::update($updated); - - // convert the current default to a non-default language - if ($updated->isDefault() === true) { - if ($oldDefault = $kirby->defaultLanguage()) { - $oldDefault->clone(['default' => false])->save(); - } - - $code = $this->code(); - $site = $kirby->site(); - - touch($site->contentFile($code)); - - foreach ($kirby->site()->index(true) as $page) { - $files = $page->files(); - - foreach ($files as $file) { - touch($file->contentFile($code)); - } - - touch($page->contentFile($code)); - } - } elseif ($this->isDefault() === true) { - throw new PermissionException('Please select another language to be the primary language'); - } - - $language = $updated->save(); - - // make sure the language is also updated in the Kirby language collection - App::instance()->languages(false)->set($language->code(), $language); - - return $language; - } + /** + * @var string + */ + protected $code; + + /** + * @var bool + */ + protected $default; + + /** + * @var string + */ + protected $direction; + + /** + * @var array + */ + protected $locale; + + /** + * @var string + */ + protected $name; + + /** + * @var array|null + */ + protected $slugs; + + /** + * @var array|null + */ + protected $smartypants; + + /** + * @var array|null + */ + protected $translations; + + /** + * @var string + */ + protected $url; + + /** + * Creates a new language object + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setRequiredProperties($props, [ + 'code' + ]); + + $this->setOptionalProperties($props, [ + 'default', + 'direction', + 'locale', + 'name', + 'slugs', + 'smartypants', + 'translations', + 'url', + ]); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Returns the language code + * when the language is converted to a string + * + * @return string + */ + public function __toString(): string + { + return $this->code(); + } + + /** + * Returns the base Url for the language + * without the path or other cruft + * + * @return string + */ + public function baseUrl(): string + { + $kirbyUrl = $this->kirby()->url(); + $languageUrl = $this->url(); + + if (empty($this->url)) { + return $kirbyUrl; + } + + if (Str::startsWith($languageUrl, $kirbyUrl) === true) { + return $kirbyUrl; + } + + return Url::base($languageUrl) ?? $kirbyUrl; + } + + /** + * Returns the language code/id. + * The language code is used in + * text file names as appendix. + * + * @return string + */ + public function code(): string + { + return $this->code; + } + + /** + * Internal converter to create or remove + * translation files. + * + * @param string $from + * @param string $to + * @return bool + */ + protected static function converter(string $from, string $to): bool + { + $kirby = App::instance(); + $site = $kirby->site(); + + // convert site + foreach ($site->files() as $file) { + F::move($file->contentFile($from, true), $file->contentFile($to, true)); + } + + F::move($site->contentFile($from, true), $site->contentFile($to, true)); + + // convert all pages + foreach ($kirby->site()->index(true) as $page) { + foreach ($page->files() as $file) { + F::move($file->contentFile($from, true), $file->contentFile($to, true)); + } + + F::move($page->contentFile($from, true), $page->contentFile($to, true)); + } + + // convert all users + foreach ($kirby->users() as $user) { + foreach ($user->files() as $file) { + F::move($file->contentFile($from, true), $file->contentFile($to, true)); + } + + F::move($user->contentFile($from, true), $user->contentFile($to, true)); + } + + return true; + } + + /** + * Creates a new language object + * + * @internal + * @param array $props + * @return static + */ + public static function create(array $props) + { + $props['code'] = Str::slug($props['code'] ?? null); + $kirby = App::instance(); + $languages = $kirby->languages(); + + // make the first language the default language + if ($languages->count() === 0) { + $props['default'] = true; + } + + $language = new static($props); + + // validate the new language + LanguageRules::create($language); + + $language->save(); + + if ($languages->count() === 0) { + static::converter('', $language->code()); + } + + // update the main languages collection in the app instance + App::instance()->languages(false)->append($language->code(), $language); + + return $language; + } + + /** + * Delete the current language and + * all its translation files + * + * @internal + * @return bool + * @throws \Kirby\Exception\Exception + */ + public function delete(): bool + { + $kirby = App::instance(); + $languages = $kirby->languages(); + $code = $this->code(); + $isLast = $languages->count() === 1; + + if (F::remove($this->root()) !== true) { + throw new Exception('The language could not be deleted'); + } + + if ($isLast === true) { + $this->converter($code, ''); + } else { + $this->deleteContentFiles($code); + } + + // get the original language collection and remove the current language + $kirby->languages(false)->remove($code); + + return true; + } + + /** + * When the language is deleted, all content files with + * the language code must be removed as well. + * + * @param mixed $code + * @return bool + */ + protected function deleteContentFiles($code): bool + { + $kirby = App::instance(); + $site = $kirby->site(); + + F::remove($site->contentFile($code, true)); + + foreach ($kirby->site()->index(true) as $page) { + foreach ($page->files() as $file) { + F::remove($file->contentFile($code, true)); + } + + F::remove($page->contentFile($code, true)); + } + + foreach ($kirby->users() as $user) { + foreach ($user->files() as $file) { + F::remove($file->contentFile($code, true)); + } + + F::remove($user->contentFile($code, true)); + } + + return true; + } + + /** + * Reading direction of this language + * + * @return string + */ + public function direction(): string + { + return $this->direction; + } + + /** + * Check if the language file exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->root()); + } + + /** + * Checks if this is the default language + * for the site. + * + * @return bool + */ + public function isDefault(): bool + { + return $this->default; + } + + /** + * The id is required for collections + * to work properly. The code is used as id + * + * @return string + */ + public function id(): string + { + return $this->code; + } + + /** + * Loads the language rules for provided locale code + * + * @param string $code + */ + public static function loadRules(string $code) + { + $kirby = App::instance(); + $code = Str::contains($code, '.') ? Str::before($code, '.') : $code; + $file = $kirby->root('i18n:rules') . '/' . $code . '.json'; + + if (F::exists($file) === false) { + $file = $kirby->root('i18n:rules') . '/' . Str::before($code, '_') . '.json'; + } + + try { + return Data::read($file); + } catch (\Exception $e) { + return []; + } + } + + /** + * Returns the PHP locale setting array + * + * @param int $category If passed, returns the locale for the specified category (e.g. LC_ALL) as string + * @return array|string + */ + public function locale(int $category = null) + { + if ($category !== null) { + return $this->locale[$category] ?? $this->locale[LC_ALL] ?? null; + } else { + return $this->locale; + } + } + + /** + * Returns the human-readable name + * of the language + * + * @return string + */ + public function name(): string + { + return $this->name; + } + + /** + * Returns the URL path for the language + * + * @return string + */ + public function path(): string + { + if ($this->url === null) { + return $this->code; + } + + return Url::path($this->url); + } + + /** + * Returns the routing pattern for the language + * + * @return string + */ + public function pattern(): string + { + $path = $this->path(); + + if (empty($path) === true) { + return '(:all)'; + } + + return $path . '/(:all?)'; + } + + /** + * Returns the absolute path to the language file + * + * @return string + */ + public function root(): string + { + return App::instance()->root('languages') . '/' . $this->code() . '.php'; + } + + /** + * Returns the LanguageRouter instance + * which is used to handle language specific + * routes. + * + * @return \Kirby\Cms\LanguageRouter + */ + public function router() + { + return new LanguageRouter($this); + } + + /** + * Get slug rules for language + * + * @internal + * @return array + */ + public function rules(): array + { + $code = $this->locale(LC_CTYPE); + $data = static::loadRules($code); + return array_merge($data, $this->slugs()); + } + + /** + * Saves the language settings in the languages folder + * + * @internal + * @return $this + */ + public function save() + { + try { + $existingData = Data::read($this->root()); + } catch (Throwable $e) { + $existingData = []; + } + + $props = [ + 'code' => $this->code(), + 'default' => $this->isDefault(), + 'direction' => $this->direction(), + 'locale' => Locale::export($this->locale()), + 'name' => $this->name(), + 'translations' => $this->translations(), + 'url' => $this->url, + ]; + + $data = array_merge($existingData, $props); + + ksort($data); + + Data::write($this->root(), $data); + + return $this; + } + + /** + * @param string $code + * @return $this + */ + protected function setCode(string $code) + { + $this->code = trim($code); + return $this; + } + + /** + * @param bool $default + * @return $this + */ + protected function setDefault(bool $default = false) + { + $this->default = $default; + return $this; + } + + /** + * @param string $direction + * @return $this + */ + protected function setDirection(string $direction = 'ltr') + { + $this->direction = $direction === 'rtl' ? 'rtl' : 'ltr'; + return $this; + } + + /** + * @param string|array $locale + * @return $this + */ + protected function setLocale($locale = null) + { + if ($locale === null) { + $this->locale = [LC_ALL => $this->code]; + } else { + $this->locale = Locale::normalize($locale); + } + + return $this; + } + + /** + * @param string $name + * @return $this + */ + protected function setName(string $name = null) + { + $this->name = trim($name ?? $this->code); + return $this; + } + + /** + * @param array $slugs + * @return $this + */ + protected function setSlugs(array $slugs = null) + { + $this->slugs = $slugs ?? []; + return $this; + } + + /** + * @param array $smartypants + * @return $this + */ + protected function setSmartypants(array $smartypants = null) + { + $this->smartypants = $smartypants ?? []; + return $this; + } + + /** + * @param array $translations + * @return $this + */ + protected function setTranslations(array $translations = null) + { + $this->translations = $translations ?? []; + return $this; + } + + /** + * @param string $url + * @return $this + */ + protected function setUrl(string $url = null) + { + $this->url = $url; + return $this; + } + + /** + * Returns the custom slug rules for this language + * + * @return array + */ + public function slugs(): array + { + return $this->slugs; + } + + /** + * Returns the custom SmartyPants options for this language + * + * @return array + */ + public function smartypants(): array + { + return $this->smartypants; + } + + /** + * Returns the most important + * properties as array + * + * @return array + */ + public function toArray(): array + { + return [ + 'code' => $this->code(), + 'default' => $this->isDefault(), + 'direction' => $this->direction(), + 'locale' => $this->locale(), + 'name' => $this->name(), + 'rules' => $this->rules(), + 'url' => $this->url() + ]; + } + + /** + * Returns the translation strings for this language + * + * @return array + */ + public function translations(): array + { + return $this->translations; + } + + /** + * Returns the absolute Url for the language + * + * @return string + */ + public function url(): string + { + $url = $this->url; + + if ($url === null) { + $url = '/' . $this->code; + } + + return Url::makeAbsolute($url, $this->kirby()->url()); + } + + /** + * Update language properties and save them + * + * @internal + * @param array $props + * @return static + */ + public function update(array $props = null) + { + // don't change the language code + unset($props['code']); + + // make sure the slug is nice and clean + $props['slug'] = Str::slug($props['slug'] ?? null); + + $kirby = App::instance(); + $updated = $this->clone($props); + + // validate the updated language + LanguageRules::update($updated); + + // convert the current default to a non-default language + if ($updated->isDefault() === true) { + if ($oldDefault = $kirby->defaultLanguage()) { + $oldDefault->clone(['default' => false])->save(); + } + + $code = $this->code(); + $site = $kirby->site(); + + touch($site->contentFile($code)); + + foreach ($kirby->site()->index(true) as $page) { + $files = $page->files(); + + foreach ($files as $file) { + touch($file->contentFile($code)); + } + + touch($page->contentFile($code)); + } + } elseif ($this->isDefault() === true) { + throw new PermissionException('Please select another language to be the primary language'); + } + + $language = $updated->save(); + + // make sure the language is also updated in the Kirby language collection + App::instance()->languages(false)->set($language->code(), $language); + + return $language; + } } diff --git a/kirby/src/Cms/LanguageRouter.php b/kirby/src/Cms/LanguageRouter.php index af35401..8fafbd7 100644 --- a/kirby/src/Cms/LanguageRouter.php +++ b/kirby/src/Cms/LanguageRouter.php @@ -20,117 +20,115 @@ use Kirby\Toolkit\Str; */ class LanguageRouter { - /** - * The parent language - * - * @var Language - */ - protected $language; + /** + * The parent language + * + * @var Language + */ + protected $language; - /** - * The router instance - * - * @var Router - */ - protected $router; + /** + * The router instance + * + * @var Router + */ + protected $router; - /** - * Creates a new language router instance - * for the given language - * - * @param \Kirby\Cms\Language $language - */ - public function __construct(Language $language) - { - $this->language = $language; - } + /** + * Creates a new language router instance + * for the given language + * + * @param \Kirby\Cms\Language $language + */ + public function __construct(Language $language) + { + $this->language = $language; + } - /** - * Fetches all scoped routes for the - * current language from the Kirby instance - * - * @return array - * @throws \Kirby\Exception\NotFoundException - */ - public function routes(): array - { - $language = $this->language; - $kirby = $language->kirby(); - $routes = $kirby->routes(); + /** + * Fetches all scoped routes for the + * current language from the Kirby instance + * + * @return array + * @throws \Kirby\Exception\NotFoundException + */ + public function routes(): array + { + $language = $this->language; + $kirby = $language->kirby(); + $routes = $kirby->routes(); - // only keep the scoped language routes - $routes = array_values(array_filter($routes, function ($route) use ($language) { + // only keep the scoped language routes + $routes = array_values(array_filter($routes, function ($route) use ($language) { + // no language scope + if (empty($route['language']) === true) { + return false; + } - // no language scope - if (empty($route['language']) === true) { - return false; - } + // wildcard + if ($route['language'] === '*') { + return true; + } - // wildcard - if ($route['language'] === '*') { - return true; - } + // get all applicable languages + $languages = Str::split(strtolower($route['language']), '|'); - // get all applicable languages - $languages = Str::split(strtolower($route['language']), '|'); + // validate the language + return in_array($language->code(), $languages) === true; + })); - // validate the language - return in_array($language->code(), $languages) === true; - })); + // add the page-scope if necessary + foreach ($routes as $index => $route) { + if ($pageId = ($route['page'] ?? null)) { + if ($page = $kirby->page($pageId)) { + // convert string patterns to arrays + $patterns = A::wrap($route['pattern']); - // add the page-scope if necessary - foreach ($routes as $index => $route) { - if ($pageId = ($route['page'] ?? null)) { - if ($page = $kirby->page($pageId)) { + // prefix all patterns with the page slug + $patterns = A::map( + $patterns, + fn ($pattern) => $page->uri($language) . '/' . $pattern + ); - // convert string patterns to arrays - $patterns = A::wrap($route['pattern']); + // re-inject the pattern and the full page object + $routes[$index]['pattern'] = $patterns; + $routes[$index]['page'] = $page; + } else { + throw new NotFoundException('The page "' . $pageId . '" does not exist'); + } + } + } - // prefix all patterns with the page slug - $patterns = A::map( - $patterns, - fn ($pattern) => $page->uri($language) . '/' . $pattern - ); + return $routes; + } - // re-inject the pattern and the full page object - $routes[$index]['pattern'] = $patterns; - $routes[$index]['page'] = $page; - } else { - throw new NotFoundException('The page "' . $pageId . '" does not exist'); - } - } - } + /** + * Wrapper around the Router::call method + * that injects the Language instance and + * if needed also the Page as arguments. + * + * @param string|null $path + * @return mixed + */ + public function call(string $path = null) + { + $language = $this->language; + $kirby = $language->kirby(); + $router = new Router($this->routes()); - return $routes; - } + try { + return $router->call($path, $kirby->request()->method(), function ($route) use ($kirby, $language) { + $kirby->setCurrentTranslation($language); + $kirby->setCurrentLanguage($language); - /** - * Wrapper around the Router::call method - * that injects the Language instance and - * if needed also the Page as arguments. - * - * @param string|null $path - * @return mixed - */ - public function call(string $path = null) - { - $language = $this->language; - $kirby = $language->kirby(); - $router = new Router($this->routes()); - - try { - return $router->call($path, $kirby->request()->method(), function ($route) use ($kirby, $language) { - $kirby->setCurrentTranslation($language); - $kirby->setCurrentLanguage($language); - - if ($page = $route->page()) { - return $route->action()->call($route, $language, $page, ...$route->arguments()); - } else { - return $route->action()->call($route, $language, ...$route->arguments()); - } - }); - } catch (Exception $e) { - return $kirby->resolve($path, $language->code()); - } - } + if ($page = $route->page()) { + return $route->action()->call($route, $language, $page, ...$route->arguments()); + } else { + return $route->action()->call($route, $language, ...$route->arguments()); + } + }); + } catch (Exception $e) { + return $kirby->resolve($path, $language->code()); + } + } } diff --git a/kirby/src/Cms/LanguageRoutes.php b/kirby/src/Cms/LanguageRoutes.php index 14801bb..6d15957 100644 --- a/kirby/src/Cms/LanguageRoutes.php +++ b/kirby/src/Cms/LanguageRoutes.php @@ -6,150 +6,147 @@ use Kirby\Filesystem\F; class LanguageRoutes { - /** - * Creates all multi-language routes - * - * @param \Kirby\Cms\App $kirby - * @return array - */ - public static function create(App $kirby): array - { - $routes = []; + /** + * Creates all multi-language routes + * + * @param \Kirby\Cms\App $kirby + * @return array + */ + public static function create(App $kirby): array + { + $routes = []; - // add the route for the home page - $routes[] = static::home($kirby); + // add the route for the home page + $routes[] = static::home($kirby); - // Kirby's base url - $baseurl = $kirby->url(); + // Kirby's base url + $baseurl = $kirby->url(); - foreach ($kirby->languages() as $language) { + foreach ($kirby->languages() as $language) { + // ignore languages with a different base url + if ($language->baseurl() !== $baseurl) { + continue; + } - // ignore languages with a different base url - if ($language->baseurl() !== $baseurl) { - continue; - } + $routes[] = [ + 'pattern' => $language->pattern(), + 'method' => 'ALL', + 'env' => 'site', + 'action' => function ($path = null) use ($language) { + if ($result = $language->router()->call($path)) { + return $result; + } - $routes[] = [ - 'pattern' => $language->pattern(), - 'method' => 'ALL', - 'env' => 'site', - 'action' => function ($path = null) use ($language) { - if ($result = $language->router()->call($path)) { - return $result; - } + // jump through to the fallback if nothing + // can be found for this language + /** @var \Kirby\Http\Route $this */ + $this->next(); + } + ]; + } - // jump through to the fallback if nothing - // can be found for this language - /** @var \Kirby\Http\Route $this */ - $this->next(); - } - ]; - } + $routes[] = static::fallback($kirby); - $routes[] = static::fallback($kirby); - - return $routes; - } + return $routes; + } - /** - * Create the fallback route - * for unprefixed default language URLs. - * - * @param \Kirby\Cms\App $kirby - * @return array - */ - public static function fallback(App $kirby): array - { - return [ - 'pattern' => '(:all)', - 'method' => 'ALL', - 'env' => 'site', - 'action' => function (string $path) use ($kirby) { + /** + * Create the fallback route + * for unprefixed default language URLs. + * + * @param \Kirby\Cms\App $kirby + * @return array + */ + public static function fallback(App $kirby): array + { + return [ + 'pattern' => '(:all)', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function (string $path) use ($kirby) { + // check for content representations or files + $extension = F::extension($path); - // check for content representations or files - $extension = F::extension($path); + // try to redirect prefixed pages + if (empty($extension) === true && $page = $kirby->page($path)) { + $url = $kirby->request()->url([ + 'query' => null, + 'params' => null, + 'fragment' => null + ]); - // try to redirect prefixed pages - if (empty($extension) === true && $page = $kirby->page($path)) { - $url = $kirby->request()->url([ - 'query' => null, - 'params' => null, - 'fragment' => null - ]); + if ($url->toString() !== $page->url()) { + // redirect to translated page directly + // if translation is exists and languages detect is enabled + if ( + $kirby->option('languages.detect') === true && + $page->translation($kirby->detectedLanguage()->code())->exists() === true + ) { + return $kirby + ->response() + ->redirect($page->url($kirby->detectedLanguage()->code())); + } - if ($url->toString() !== $page->url()) { - // redirect to translated page directly - // if translation is exists and languages detect is enabled - if ( - $kirby->option('languages.detect') === true && - $page->translation($kirby->detectedLanguage()->code())->exists() === true - ) { - return $kirby - ->response() - ->redirect($page->url($kirby->detectedLanguage()->code())); - } + return $kirby + ->response() + ->redirect($page->url()); + } + } - return $kirby - ->response() - ->redirect($page->url()); - } - } + return $kirby->language()->router()->call($path); + } + ]; + } - return $kirby->language()->router()->call($path); - } - ]; - } + /** + * Create the multi-language home page route + * + * @param \Kirby\Cms\App $kirby + * @return array + */ + public static function home(App $kirby): array + { + // Multi-language home + return [ + 'pattern' => '', + 'method' => 'ALL', + 'env' => 'site', + 'action' => function () use ($kirby) { + // find all languages with the same base url as the current installation + $languages = $kirby->languages()->filter('baseurl', $kirby->url()); - /** - * Create the multi-language home page route - * - * @param \Kirby\Cms\App $kirby - * @return array - */ - public static function home(App $kirby): array - { - // Multi-language home - return [ - 'pattern' => '', - 'method' => 'ALL', - 'env' => 'site', - 'action' => function () use ($kirby) { + // if there's no language with a matching base url, + // redirect to the default language + if ($languages->count() === 0) { + return $kirby + ->response() + ->redirect($kirby->defaultLanguage()->url()); + } - // find all languages with the same base url as the current installation - $languages = $kirby->languages()->filter('baseurl', $kirby->url()); + // if there's just one language, we take that to render the home page + if ($languages->count() === 1) { + $currentLanguage = $languages->first(); + } else { + $currentLanguage = $kirby->defaultLanguage(); + } - // if there's no language with a matching base url, - // redirect to the default language - if ($languages->count() === 0) { - return $kirby - ->response() - ->redirect($kirby->defaultLanguage()->url()); - } + // language detection on the home page with / as URL + if ($kirby->url() !== $currentLanguage->url()) { + if ($kirby->option('languages.detect') === true) { + return $kirby + ->response() + ->redirect($kirby->detectedLanguage()->url()); + } - // if there's just one language, we take that to render the home page - if ($languages->count() === 1) { - $currentLanguage = $languages->first(); - } else { - $currentLanguage = $kirby->defaultLanguage(); - } + return $kirby + ->response() + ->redirect($currentLanguage->url()); + } - // language detection on the home page with / as URL - if ($kirby->url() !== $currentLanguage->url()) { - if ($kirby->option('languages.detect') === true) { - return $kirby - ->response() - ->redirect($kirby->detectedLanguage()->url()); - } - - return $kirby - ->response() - ->redirect($currentLanguage->url()); - } - - // render the home page of the current language - return $currentLanguage->router()->call(); - } - ]; - } + // render the home page of the current language + return $currentLanguage->router()->call(); + } + ]; + } } diff --git a/kirby/src/Cms/LanguageRules.php b/kirby/src/Cms/LanguageRules.php index d8887ae..31c751e 100644 --- a/kirby/src/Cms/LanguageRules.php +++ b/kirby/src/Cms/LanguageRules.php @@ -17,82 +17,82 @@ use Kirby\Toolkit\Str; */ class LanguageRules { - /** - * Validates if the language can be created - * - * @param \Kirby\Cms\Language $language - * @return bool - * @throws \Kirby\Exception\DuplicateException If the language already exists - */ - public static function create(Language $language): bool - { - static::validLanguageCode($language); - static::validLanguageName($language); + /** + * Validates if the language can be created + * + * @param \Kirby\Cms\Language $language + * @return bool + * @throws \Kirby\Exception\DuplicateException If the language already exists + */ + public static function create(Language $language): bool + { + static::validLanguageCode($language); + static::validLanguageName($language); - if ($language->exists() === true) { - throw new DuplicateException([ - 'key' => 'language.duplicate', - 'data' => [ - 'code' => $language->code() - ] - ]); - } + if ($language->exists() === true) { + throw new DuplicateException([ + 'key' => 'language.duplicate', + 'data' => [ + 'code' => $language->code() + ] + ]); + } - return true; - } + return true; + } - /** - * Validates if the language can be updated - * - * @param \Kirby\Cms\Language $language - */ - public static function update(Language $language) - { - static::validLanguageCode($language); - static::validLanguageName($language); - } + /** + * Validates if the language can be updated + * + * @param \Kirby\Cms\Language $language + */ + public static function update(Language $language) + { + static::validLanguageCode($language); + static::validLanguageName($language); + } - /** - * Validates if the language code is formatted correctly - * - * @param \Kirby\Cms\Language $language - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the language code is not valid - */ - public static function validLanguageCode(Language $language): bool - { - if (Str::length($language->code()) < 2) { - throw new InvalidArgumentException([ - 'key' => 'language.code', - 'data' => [ - 'code' => $language->code(), - 'name' => $language->name() - ] - ]); - } + /** + * Validates if the language code is formatted correctly + * + * @param \Kirby\Cms\Language $language + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the language code is not valid + */ + public static function validLanguageCode(Language $language): bool + { + if (Str::length($language->code()) < 2) { + throw new InvalidArgumentException([ + 'key' => 'language.code', + 'data' => [ + 'code' => $language->code(), + 'name' => $language->name() + ] + ]); + } - return true; - } + return true; + } - /** - * Validates if the language name is formatted correctly - * - * @param \Kirby\Cms\Language $language - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the language name is invalid - */ - public static function validLanguageName(Language $language): bool - { - if (Str::length($language->name()) < 1) { - throw new InvalidArgumentException([ - 'key' => 'language.name', - 'data' => [ - 'code' => $language->code(), - 'name' => $language->name() - ] - ]); - } + /** + * Validates if the language name is formatted correctly + * + * @param \Kirby\Cms\Language $language + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the language name is invalid + */ + public static function validLanguageName(Language $language): bool + { + if (Str::length($language->name()) < 1) { + throw new InvalidArgumentException([ + 'key' => 'language.name', + 'data' => [ + 'code' => $language->code(), + 'name' => $language->name() + ] + ]); + } - return true; - } + return true; + } } diff --git a/kirby/src/Cms/Languages.php b/kirby/src/Cms/Languages.php index 2b07096..d746977 100644 --- a/kirby/src/Cms/Languages.php +++ b/kirby/src/Cms/Languages.php @@ -16,86 +16,86 @@ use Kirby\Filesystem\F; */ class Languages extends Collection { - /** - * Creates a new collection with the given language objects - * - * @param array $objects `Kirby\Cms\Language` objects - * @param null $parent - * @throws \Kirby\Exception\DuplicateException - */ - public function __construct($objects = [], $parent = null) - { - $defaults = array_filter( - $objects, - fn ($language) => $language->isDefault() === true - ); + /** + * Creates a new collection with the given language objects + * + * @param array $objects `Kirby\Cms\Language` objects + * @param null $parent + * @throws \Kirby\Exception\DuplicateException + */ + public function __construct($objects = [], $parent = null) + { + $defaults = array_filter( + $objects, + fn ($language) => $language->isDefault() === true + ); - if (count($defaults) > 1) { - throw new DuplicateException('You cannot have multiple default languages. Please check your language config files.'); - } + if (count($defaults) > 1) { + throw new DuplicateException('You cannot have multiple default languages. Please check your language config files.'); + } - parent::__construct($objects, $parent); - } + parent::__construct($objects, $parent); + } - /** - * Returns all language codes as array - * - * @return array - */ - public function codes(): array - { - return $this->keys(); - } + /** + * Returns all language codes as array + * + * @return array + */ + public function codes(): array + { + return $this->keys(); + } - /** - * Creates a new language with the given props - * - * @internal - * @param array $props - * @return \Kirby\Cms\Language - */ - public function create(array $props) - { - return Language::create($props); - } + /** + * Creates a new language with the given props + * + * @internal + * @param array $props + * @return \Kirby\Cms\Language + */ + public function create(array $props) + { + return Language::create($props); + } - /** - * Returns the default language - * - * @return \Kirby\Cms\Language|null - */ - public function default() - { - if ($language = $this->findBy('isDefault', true)) { - return $language; - } else { - return $this->first(); - } - } + /** + * Returns the default language + * + * @return \Kirby\Cms\Language|null + */ + public function default() + { + if ($language = $this->findBy('isDefault', true)) { + return $language; + } else { + return $this->first(); + } + } - /** - * Convert all defined languages to a collection - * - * @internal - * @return static - */ - public static function load() - { - $languages = []; - $files = glob(App::instance()->root('languages') . '/*.php'); + /** + * Convert all defined languages to a collection + * + * @internal + * @return static + */ + public static function load() + { + $languages = []; + $files = glob(App::instance()->root('languages') . '/*.php'); - foreach ($files as $file) { - $props = F::load($file); + foreach ($files as $file) { + $props = F::load($file); - if (is_array($props) === true) { - // inject the language code from the filename - // if it does not exist - $props['code'] ??= F::name($file); + if (is_array($props) === true) { + // inject the language code from the filename + // if it does not exist + $props['code'] ??= F::name($file); - $languages[] = new Language($props); - } - } + $languages[] = new Language($props); + } + } - return new static($languages); - } + return new static($languages); + } } diff --git a/kirby/src/Cms/Layout.php b/kirby/src/Cms/Layout.php index 22bbf06..9a27fbf 100644 --- a/kirby/src/Cms/Layout.php +++ b/kirby/src/Cms/Layout.php @@ -15,113 +15,113 @@ namespace Kirby\Cms; */ class Layout extends Item { - use HasMethods; + use HasMethods; - public const ITEMS_CLASS = '\Kirby\Cms\Layouts'; + public const ITEMS_CLASS = '\Kirby\Cms\Layouts'; - /** - * @var \Kirby\Cms\Content - */ - protected $attrs; + /** + * @var \Kirby\Cms\Content + */ + protected $attrs; - /** - * @var \Kirby\Cms\LayoutColumns - */ - protected $columns; + /** + * @var \Kirby\Cms\LayoutColumns + */ + protected $columns; - /** - * Proxy for attrs - * - * @param string $method - * @param array $args - * @return \Kirby\Cms\Field - */ - public function __call(string $method, array $args = []) - { - // layout methods - if ($this->hasMethod($method) === true) { - return $this->callMethod($method, $args); - } + /** + * Proxy for attrs + * + * @param string $method + * @param array $args + * @return \Kirby\Cms\Field + */ + public function __call(string $method, array $args = []) + { + // layout methods + if ($this->hasMethod($method) === true) { + return $this->callMethod($method, $args); + } - return $this->attrs()->get($method); - } + return $this->attrs()->get($method); + } - /** - * Creates a new Layout object - * - * @param array $params - */ - public function __construct(array $params = []) - { - parent::__construct($params); + /** + * Creates a new Layout object + * + * @param array $params + */ + public function __construct(array $params = []) + { + parent::__construct($params); - $this->columns = LayoutColumns::factory($params['columns'] ?? [], [ - 'parent' => $this->parent - ]); + $this->columns = LayoutColumns::factory($params['columns'] ?? [], [ + 'parent' => $this->parent + ]); - // create the attrs object - $this->attrs = new Content($params['attrs'] ?? [], $this->parent); - } + // create the attrs object + $this->attrs = new Content($params['attrs'] ?? [], $this->parent); + } - /** - * Returns the attrs object - * - * @return \Kirby\Cms\Content - */ - public function attrs() - { - return $this->attrs; - } + /** + * Returns the attrs object + * + * @return \Kirby\Cms\Content + */ + public function attrs() + { + return $this->attrs; + } - /** - * Returns the columns in this layout - * - * @return \Kirby\Cms\LayoutColumns - */ - public function columns() - { - return $this->columns; - } + /** + * Returns the columns in this layout + * + * @return \Kirby\Cms\LayoutColumns + */ + public function columns() + { + return $this->columns; + } - /** - * Checks if the layout is empty - * @since 3.5.2 - * - * @return bool - */ - public function isEmpty(): bool - { - return $this - ->columns() - ->filter(function ($column) { - return $column->isNotEmpty(); - }) - ->count() === 0; - } + /** + * Checks if the layout is empty + * @since 3.5.2 + * + * @return bool + */ + public function isEmpty(): bool + { + return $this + ->columns() + ->filter(function ($column) { + return $column->isNotEmpty(); + }) + ->count() === 0; + } - /** - * Checks if the layout is not empty - * @since 3.5.2 - * - * @return bool - */ - public function isNotEmpty(): bool - { - return $this->isEmpty() === false; - } + /** + * Checks if the layout is not empty + * @since 3.5.2 + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->isEmpty() === false; + } - /** - * The result is being sent to the editor - * via the API in the panel - * - * @return array - */ - public function toArray(): array - { - return [ - 'attrs' => $this->attrs()->toArray(), - 'columns' => $this->columns()->toArray(), - 'id' => $this->id(), - ]; - } + /** + * The result is being sent to the editor + * via the API in the panel + * + * @return array + */ + public function toArray(): array + { + return [ + 'attrs' => $this->attrs()->toArray(), + 'columns' => $this->columns()->toArray(), + 'id' => $this->id(), + ]; + } } diff --git a/kirby/src/Cms/LayoutColumn.php b/kirby/src/Cms/LayoutColumn.php index 1a33ab9..2f652d1 100644 --- a/kirby/src/Cms/LayoutColumn.php +++ b/kirby/src/Cms/LayoutColumn.php @@ -17,128 +17,128 @@ use Kirby\Toolkit\Str; */ class LayoutColumn extends Item { - use HasMethods; + use HasMethods; - public const ITEMS_CLASS = '\Kirby\Cms\LayoutColumns'; + public const ITEMS_CLASS = '\Kirby\Cms\LayoutColumns'; - /** - * @var \Kirby\Cms\Blocks - */ - protected $blocks; + /** + * @var \Kirby\Cms\Blocks + */ + protected $blocks; - /** - * @var string - */ - protected $width; + /** + * @var string + */ + protected $width; - /** - * Creates a new LayoutColumn object - * - * @param array $params - */ - public function __construct(array $params = []) - { - parent::__construct($params); + /** + * Creates a new LayoutColumn object + * + * @param array $params + */ + public function __construct(array $params = []) + { + parent::__construct($params); - $this->blocks = Blocks::factory($params['blocks'] ?? [], [ - 'parent' => $this->parent - ]); + $this->blocks = Blocks::factory($params['blocks'] ?? [], [ + 'parent' => $this->parent + ]); - $this->width = $params['width'] ?? '1/1'; - } + $this->width = $params['width'] ?? '1/1'; + } - /** - * Magic getter function - * - * @param string $method - * @param mixed $args - * @return mixed - */ - public function __call(string $method, $args) - { - // layout column methods - if ($this->hasMethod($method) === true) { - return $this->callMethod($method, $args); - } - } + /** + * Magic getter function + * + * @param string $method + * @param mixed $args + * @return mixed + */ + public function __call(string $method, $args) + { + // layout column methods + if ($this->hasMethod($method) === true) { + return $this->callMethod($method, $args); + } + } - /** - * Returns the blocks collection - * - * @param bool $includeHidden Sets whether to include hidden blocks - * @return \Kirby\Cms\Blocks - */ - public function blocks(bool $includeHidden = false) - { - if ($includeHidden === false) { - return $this->blocks->filter('isHidden', false); - } + /** + * Returns the blocks collection + * + * @param bool $includeHidden Sets whether to include hidden blocks + * @return \Kirby\Cms\Blocks + */ + public function blocks(bool $includeHidden = false) + { + if ($includeHidden === false) { + return $this->blocks->filter('isHidden', false); + } - return $this->blocks; - } + return $this->blocks; + } - /** - * Checks if the column is empty - * @since 3.5.2 - * - * @return bool - */ - public function isEmpty(): bool - { - return $this - ->blocks() - ->filter('isHidden', false) - ->count() === 0; - } + /** + * Checks if the column is empty + * @since 3.5.2 + * + * @return bool + */ + public function isEmpty(): bool + { + return $this + ->blocks() + ->filter('isHidden', false) + ->count() === 0; + } - /** - * Checks if the column is not empty - * @since 3.5.2 - * - * @return bool - */ - public function isNotEmpty(): bool - { - return $this->isEmpty() === false; - } + /** + * Checks if the column is not empty + * @since 3.5.2 + * + * @return bool + */ + public function isNotEmpty(): bool + { + return $this->isEmpty() === false; + } - /** - * Returns the number of columns this column spans - * - * @param int $columns - * @return int - */ - public function span(int $columns = 12): int - { - $fraction = Str::split($this->width, '/'); - $a = $fraction[0] ?? 1; - $b = $fraction[1] ?? 1; + /** + * Returns the number of columns this column spans + * + * @param int $columns + * @return int + */ + public function span(int $columns = 12): int + { + $fraction = Str::split($this->width, '/'); + $a = $fraction[0] ?? 1; + $b = $fraction[1] ?? 1; - return $columns * $a / $b; - } + return $columns * $a / $b; + } - /** - * The result is being sent to the editor - * via the API in the panel - * - * @return array - */ - public function toArray(): array - { - return [ - 'blocks' => $this->blocks(true)->toArray(), - 'id' => $this->id(), - 'width' => $this->width(), - ]; - } + /** + * The result is being sent to the editor + * via the API in the panel + * + * @return array + */ + public function toArray(): array + { + return [ + 'blocks' => $this->blocks(true)->toArray(), + 'id' => $this->id(), + 'width' => $this->width(), + ]; + } - /** - * Returns the width of the column - * - * @return string - */ - public function width(): string - { - return $this->width; - } + /** + * Returns the width of the column + * + * @return string + */ + public function width(): string + { + return $this->width; + } } diff --git a/kirby/src/Cms/LayoutColumns.php b/kirby/src/Cms/LayoutColumns.php index 1ebab67..449678b 100644 --- a/kirby/src/Cms/LayoutColumns.php +++ b/kirby/src/Cms/LayoutColumns.php @@ -14,5 +14,5 @@ namespace Kirby\Cms; */ class LayoutColumns extends Items { - public const ITEM_CLASS = '\Kirby\Cms\LayoutColumn'; + public const ITEM_CLASS = '\Kirby\Cms\LayoutColumn'; } diff --git a/kirby/src/Cms/Layouts.php b/kirby/src/Cms/Layouts.php index 512f175..b9f4c90 100644 --- a/kirby/src/Cms/Layouts.php +++ b/kirby/src/Cms/Layouts.php @@ -3,6 +3,7 @@ namespace Kirby\Cms; use Kirby\Data\Data; +use Kirby\Toolkit\Str; use Throwable; /** @@ -17,86 +18,86 @@ use Throwable; */ class Layouts extends Items { - public const ITEM_CLASS = '\Kirby\Cms\Layout'; + public const ITEM_CLASS = '\Kirby\Cms\Layout'; - public static function factory(array $items = null, array $params = []) - { - $first = $items[0] ?? []; + public static function factory(array $items = null, array $params = []) + { + $first = $items[0] ?? []; - // if there are no wrapping layouts for blocks yet … - if (array_key_exists('content', $first) === true || array_key_exists('type', $first) === true) { - $items = [ - [ - 'id' => uuid(), - 'columns' => [ - [ - 'width' => '1/1', - 'blocks' => $items - ] - ] - ] - ]; - } + // if there are no wrapping layouts for blocks yet … + if (array_key_exists('content', $first) === true || array_key_exists('type', $first) === true) { + $items = [ + [ + 'id' => Str::uuid(), + 'columns' => [ + [ + 'width' => '1/1', + 'blocks' => $items + ] + ] + ] + ]; + } - return parent::factory($items, $params); - } + return parent::factory($items, $params); + } - /** - * Checks if a given block type exists in the layouts collection - * @since 3.6.0 - * - * @param string $type - * @return bool - */ - public function hasBlockType(string $type): bool - { - return $this->toBlocks()->hasType($type); - } + /** + * Checks if a given block type exists in the layouts collection + * @since 3.6.0 + * + * @param string $type + * @return bool + */ + public function hasBlockType(string $type): bool + { + return $this->toBlocks()->hasType($type); + } - /** - * Parse layouts data - * - * @param array|string $input - * @return array - */ - public static function parse($input): array - { - if (empty($input) === false && is_array($input) === false) { - try { - $input = Data::decode($input, 'json'); - } catch (Throwable $e) { - return []; - } - } + /** + * Parse layouts data + * + * @param array|string $input + * @return array + */ + public static function parse($input): array + { + if (empty($input) === false && is_array($input) === false) { + try { + $input = Data::decode($input, 'json'); + } catch (Throwable $e) { + return []; + } + } - if (empty($input) === true) { - return []; - } + if (empty($input) === true) { + return []; + } - return $input; - } + return $input; + } - /** - * Converts layouts to blocks - * @since 3.6.0 - * - * @param bool $includeHidden Sets whether to include hidden blocks - * @return \Kirby\Cms\Blocks - */ - public function toBlocks(bool $includeHidden = false) - { - $blocks = []; + /** + * Converts layouts to blocks + * @since 3.6.0 + * + * @param bool $includeHidden Sets whether to include hidden blocks + * @return \Kirby\Cms\Blocks + */ + public function toBlocks(bool $includeHidden = false) + { + $blocks = []; - if ($this->isNotEmpty() === true) { - foreach ($this->data() as $layout) { - foreach ($layout->columns() as $column) { - foreach ($column->blocks($includeHidden) as $block) { - $blocks[] = $block->toArray(); - } - } - } - } + if ($this->isNotEmpty() === true) { + foreach ($this->data() as $layout) { + foreach ($layout->columns() as $column) { + foreach ($column->blocks($includeHidden) as $block) { + $blocks[] = $block->toArray(); + } + } + } + } - return Blocks::factory($blocks); - } + return Blocks::factory($blocks); + } } diff --git a/kirby/src/Cms/Loader.php b/kirby/src/Cms/Loader.php index 611727c..0f0cbc4 100644 --- a/kirby/src/Cms/Loader.php +++ b/kirby/src/Cms/Loader.php @@ -28,223 +28,223 @@ use Kirby\Filesystem\F; */ class Loader { - /** - * @var \Kirby\Cms\App - */ - protected $kirby; + /** + * @var \Kirby\Cms\App + */ + protected $kirby; - /** - * @var bool - */ - protected $withPlugins; + /** + * @var bool + */ + protected $withPlugins; - /** - * @param \Kirby\Cms\App $kirby - * @param bool $withPlugins - */ - public function __construct(App $kirby, bool $withPlugins = true) - { - $this->kirby = $kirby; - $this->withPlugins = $withPlugins; - } + /** + * @param \Kirby\Cms\App $kirby + * @param bool $withPlugins + */ + public function __construct(App $kirby, bool $withPlugins = true) + { + $this->kirby = $kirby; + $this->withPlugins = $withPlugins; + } - /** - * Loads the area definition - * - * @param string $name - * @return array|null - */ - public function area(string $name): ?array - { - return $this->areas()[$name] ?? null; - } + /** + * Loads the area definition + * + * @param string $name + * @return array|null + */ + public function area(string $name): ?array + { + return $this->areas()[$name] ?? null; + } - /** - * Loads all areas and makes sure that plugins - * are injected properly - * - * @return array - */ - public function areas(): array - { - $areas = []; - $extensions = $this->withPlugins === true ? $this->kirby->extensions('areas') : []; + /** + * Loads all areas and makes sure that plugins + * are injected properly + * + * @return array + */ + public function areas(): array + { + $areas = []; + $extensions = $this->withPlugins === true ? $this->kirby->extensions('areas') : []; - // load core areas and extend them with elements from plugins if they exist - foreach ($this->kirby->core()->areas() as $id => $area) { - $area = $this->resolveArea($area); + // load core areas and extend them with elements from plugins if they exist + foreach ($this->kirby->core()->areas() as $id => $area) { + $area = $this->resolveArea($area); - if (isset($extensions[$id]) === true) { - foreach ($extensions[$id] as $areaExtension) { - $extension = $this->resolveArea($areaExtension); - $area = array_replace_recursive($area, $extension); - } + if (isset($extensions[$id]) === true) { + foreach ($extensions[$id] as $areaExtension) { + $extension = $this->resolveArea($areaExtension); + $area = array_replace_recursive($area, $extension); + } - unset($extensions[$id]); - } + unset($extensions[$id]); + } - $areas[$id] = $area; - } + $areas[$id] = $area; + } - // add additional areas from plugins - foreach ($extensions as $id => $areaExtensions) { - foreach ($areaExtensions as $areaExtension) { - $areas[$id] = $this->resolve($areaExtension); - } - } + // add additional areas from plugins + foreach ($extensions as $id => $areaExtensions) { + foreach ($areaExtensions as $areaExtension) { + $areas[$id] = $this->resolve($areaExtension); + } + } - return $areas; - } + return $areas; + } - /** - * Loads a core component closure - * - * @param string $name - * @return \Closure|null - */ - public function component(string $name): ?Closure - { - return $this->extension('components', $name); - } + /** + * Loads a core component closure + * + * @param string $name + * @return \Closure|null + */ + public function component(string $name): ?Closure + { + return $this->extension('components', $name); + } - /** - * Loads all core component closures - * - * @return array - */ - public function components(): array - { - return $this->extensions('components'); - } + /** + * Loads all core component closures + * + * @return array + */ + public function components(): array + { + return $this->extensions('components'); + } - /** - * Loads a particular extension - * - * @param string $type - * @param string $name - * @return mixed - */ - public function extension(string $type, string $name) - { - return $this->extensions($type)[$name] ?? null; - } + /** + * Loads a particular extension + * + * @param string $type + * @param string $name + * @return mixed + */ + public function extension(string $type, string $name) + { + return $this->extensions($type)[$name] ?? null; + } - /** - * Loads all defined extensions - * - * @param string $type - * @return array - */ - public function extensions(string $type): array - { - return $this->withPlugins === false ? $this->kirby->core()->$type() : $this->kirby->extensions($type); - } + /** + * Loads all defined extensions + * + * @param string $type + * @return array + */ + public function extensions(string $type): array + { + return $this->withPlugins === false ? $this->kirby->core()->$type() : $this->kirby->extensions($type); + } - /** - * The resolver takes a string, array or closure. - * - * 1.) a string is supposed to be a path to an existing file. - * The file will either be included when it's a PHP file and - * the array contents will be read. Or it will be parsed with - * the Data class to read yml or json data into an array - * - * 2.) arrays are untouched and returned - * - * 3.) closures will be called and the Kirby instance will be - * passed as first argument - * - * @param mixed $item - * @return mixed - */ - public function resolve($item) - { - if (is_string($item) === true) { - if (F::extension($item) !== 'php') { - $item = Data::read($item); - } else { - $item = require $item; - } - } + /** + * The resolver takes a string, array or closure. + * + * 1.) a string is supposed to be a path to an existing file. + * The file will either be included when it's a PHP file and + * the array contents will be read. Or it will be parsed with + * the Data class to read yml or json data into an array + * + * 2.) arrays are untouched and returned + * + * 3.) closures will be called and the Kirby instance will be + * passed as first argument + * + * @param mixed $item + * @return mixed + */ + public function resolve($item) + { + if (is_string($item) === true) { + if (F::extension($item) !== 'php') { + $item = Data::read($item); + } else { + $item = require $item; + } + } - if (is_callable($item)) { - $item = $item($this->kirby); - } + if (is_callable($item)) { + $item = $item($this->kirby); + } - return $item; - } + return $item; + } - /** - * Calls `static::resolve()` on all items - * in the given array - * - * @param array $items - * @return array - */ - public function resolveAll(array $items): array - { - $result = []; + /** + * Calls `static::resolve()` on all items + * in the given array + * + * @param array $items + * @return array + */ + public function resolveAll(array $items): array + { + $result = []; - foreach ($items as $key => $value) { - $result[$key] = $this->resolve($value); - } + foreach ($items as $key => $value) { + $result[$key] = $this->resolve($value); + } - return $result; - } + return $result; + } - /** - * Areas need a bit of special treatment - * when they are being loaded - * - * @param string|array|Closure $area - * @return array - */ - public function resolveArea($area): array - { - $area = $this->resolve($area); - $dropdowns = $area['dropdowns'] ?? []; + /** + * Areas need a bit of special treatment + * when they are being loaded + * + * @param string|array|Closure $area + * @return array + */ + public function resolveArea($area): array + { + $area = $this->resolve($area); + $dropdowns = $area['dropdowns'] ?? []; - // convert closure dropdowns to an array definition - // otherwise they cannot be merged properly later - foreach ($dropdowns as $key => $dropdown) { - if (is_a($dropdown, 'Closure') === true) { - $area['dropdowns'][$key] = [ - 'options' => $dropdown - ]; - } - } + // convert closure dropdowns to an array definition + // otherwise they cannot be merged properly later + foreach ($dropdowns as $key => $dropdown) { + if (is_a($dropdown, 'Closure') === true) { + $area['dropdowns'][$key] = [ + 'options' => $dropdown + ]; + } + } - return $area; - } + return $area; + } - /** - * Loads a particular section definition - * - * @param string $name - * @return array|null - */ - public function section(string $name): ?array - { - return $this->resolve($this->extension('sections', $name)); - } + /** + * Loads a particular section definition + * + * @param string $name + * @return array|null + */ + public function section(string $name): ?array + { + return $this->resolve($this->extension('sections', $name)); + } - /** - * Loads all section defintions - * - * @return array - */ - public function sections(): array - { - return $this->resolveAll($this->extensions('sections')); - } + /** + * Loads all section defintions + * + * @return array + */ + public function sections(): array + { + return $this->resolveAll($this->extensions('sections')); + } - /** - * Returns the status flag, which shows - * if plugins are loaded as well. - * - * @return bool - */ - public function withPlugins(): bool - { - return $this->withPlugins; - } + /** + * Returns the status flag, which shows + * if plugins are loaded as well. + * + * @return bool + */ + public function withPlugins(): bool + { + return $this->withPlugins; + } } diff --git a/kirby/src/Cms/Media.php b/kirby/src/Cms/Media.php index 1520ab9..32da5af 100644 --- a/kirby/src/Cms/Media.php +++ b/kirby/src/Cms/Media.php @@ -20,153 +20,152 @@ use Throwable; */ class Media { - /** - * Tries to find a file by model and filename - * and to copy it to the media folder. - * - * @param \Kirby\Cms\Model|null $model - * @param string $hash - * @param string $filename - * @return \Kirby\Cms\Response|false - */ - public static function link(Model $model = null, string $hash, string $filename) - { - if ($model === null) { - return false; - } + /** + * Tries to find a file by model and filename + * and to copy it to the media folder. + * + * @param \Kirby\Cms\Model|null $model + * @param string $hash + * @param string $filename + * @return \Kirby\Cms\Response|false + */ + public static function link(Model $model = null, string $hash, string $filename) + { + if ($model === null) { + return false; + } - // fix issues with spaces in filenames - $filename = urldecode($filename); + // fix issues with spaces in filenames + $filename = urldecode($filename); - // try to find a file by model and filename - // this should work for all original files - if ($file = $model->file($filename)) { + // try to find a file by model and filename + // this should work for all original files + if ($file = $model->file($filename)) { + // check if the request contained an outdated media hash + if ($file->mediaHash() !== $hash) { + // if at least the token was correct, redirect + if (Str::startsWith($hash, $file->mediaToken() . '-') === true) { + return Response::redirect($file->mediaUrl(), 307); + } else { + // don't leak the correct token, render the error page + return false; + } + } - // check if the request contained an outdated media hash - if ($file->mediaHash() !== $hash) { - // if at least the token was correct, redirect - if (Str::startsWith($hash, $file->mediaToken() . '-') === true) { - return Response::redirect($file->mediaUrl(), 307); - } else { - // don't leak the correct token, render the error page - return false; - } - } + // send the file to the browser + return Response::file($file->publish()->mediaRoot()); + } - // send the file to the browser - return Response::file($file->publish()->mediaRoot()); - } + // try to generate a thumb for the file + return static::thumb($model, $hash, $filename); + } - // try to generate a thumb for the file - return static::thumb($model, $hash, $filename); - } + /** + * Copy the file to the final media folder location + * + * @param \Kirby\Cms\File $file + * @param string $dest + * @return bool + */ + public static function publish(File $file, string $dest): bool + { + // never publish risky files (e.g. HTML, PHP or Apache config files) + FileRules::validFile($file, false); - /** - * Copy the file to the final media folder location - * - * @param \Kirby\Cms\File $file - * @param string $dest - * @return bool - */ - public static function publish(File $file, string $dest): bool - { - // never publish risky files (e.g. HTML, PHP or Apache config files) - FileRules::validFile($file, false); + $src = $file->root(); + $version = dirname($dest); + $directory = dirname($version); - $src = $file->root(); - $version = dirname($dest); - $directory = dirname($version); + // unpublish all files except stuff in the version folder + Media::unpublish($directory, $file, $version); - // unpublish all files except stuff in the version folder - Media::unpublish($directory, $file, $version); + // copy/overwrite the file to the dest folder + return F::copy($src, $dest, true); + } - // copy/overwrite the file to the dest folder - return F::copy($src, $dest, true); - } + /** + * Tries to find a job file for the + * given filename and then calls the thumb + * component to create a thumbnail accordingly + * + * @param \Kirby\Cms\Model|string $model + * @param string $hash + * @param string $filename + * @return \Kirby\Cms\Response|false + */ + public static function thumb($model, string $hash, string $filename) + { + $kirby = App::instance(); - /** - * Tries to find a job file for the - * given filename and then calls the thumb - * component to create a thumbnail accordingly - * - * @param \Kirby\Cms\Model|string $model - * @param string $hash - * @param string $filename - * @return \Kirby\Cms\Response|false - */ - public static function thumb($model, string $hash, string $filename) - { - $kirby = App::instance(); + // assets + if (is_string($model) === true) { + $root = $kirby->root('media') . '/assets/' . $model . '/' . $hash; + // parent files for file model that already included hash + } elseif (is_a($model, '\Kirby\Cms\File')) { + $root = dirname($model->mediaRoot()); + // model files + } else { + $root = $model->mediaRoot() . '/' . $hash; + } - // assets - if (is_string($model) === true) { - $root = $kirby->root('media') . '/assets/' . $model . '/' . $hash; - // parent files for file model that already included hash - } elseif (is_a($model, '\Kirby\Cms\File')) { - $root = dirname($model->mediaRoot()); - // model files - } else { - $root = $model->mediaRoot() . '/' . $hash; - } + try { + $thumb = $root . '/' . $filename; + $job = $root . '/.jobs/' . $filename . '.json'; + $options = Data::read($job); - try { - $thumb = $root . '/' . $filename; - $job = $root . '/.jobs/' . $filename . '.json'; - $options = Data::read($job); + if (empty($options) === true) { + return false; + } - if (empty($options) === true) { - return false; - } + if (is_string($model) === true) { + $source = $kirby->root('index') . '/' . $model . '/' . $options['filename']; + } else { + $source = $model->file($options['filename'])->root(); + } - if (is_string($model) === true) { - $source = $kirby->root('index') . '/' . $model . '/' . $options['filename']; - } else { - $source = $model->file($options['filename'])->root(); - } + try { + $kirby->thumb($source, $thumb, $options); + F::remove($job); + return Response::file($thumb); + } catch (Throwable $e) { + F::remove($thumb); + return Response::file($source); + } + } catch (Throwable $e) { + return false; + } + } - try { - $kirby->thumb($source, $thumb, $options); - F::remove($job); - return Response::file($thumb); - } catch (Throwable $e) { - F::remove($thumb); - return Response::file($source); - } - } catch (Throwable $e) { - return false; - } - } + /** + * Deletes all versions of the given file + * within the parent directory + * + * @param string $directory + * @param \Kirby\Cms\File $file + * @param string|null $ignore + * @return bool + */ + public static function unpublish(string $directory, File $file, string $ignore = null): bool + { + if (is_dir($directory) === false) { + return true; + } - /** - * Deletes all versions of the given file - * within the parent directory - * - * @param string $directory - * @param \Kirby\Cms\File $file - * @param string|null $ignore - * @return bool - */ - public static function unpublish(string $directory, File $file, string $ignore = null): bool - { - if (is_dir($directory) === false) { - return true; - } + // get both old and new versions (pre and post Kirby 3.4.0) + $versions = array_merge( + glob($directory . '/' . crc32($file->filename()) . '-*', GLOB_ONLYDIR), + glob($directory . '/' . $file->mediaToken() . '-*', GLOB_ONLYDIR) + ); - // get both old and new versions (pre and post Kirby 3.4.0) - $versions = array_merge( - glob($directory . '/' . crc32($file->filename()) . '-*', GLOB_ONLYDIR), - glob($directory . '/' . $file->mediaToken() . '-*', GLOB_ONLYDIR) - ); + // delete all versions of the file + foreach ($versions as $version) { + if ($version === $ignore) { + continue; + } - // delete all versions of the file - foreach ($versions as $version) { - if ($version === $ignore) { - continue; - } + Dir::remove($version); + } - Dir::remove($version); - } - - return true; - } + return true; + } } diff --git a/kirby/src/Cms/Model.php b/kirby/src/Cms/Model.php index 95b83d8..94c0317 100644 --- a/kirby/src/Cms/Model.php +++ b/kirby/src/Cms/Model.php @@ -15,103 +15,103 @@ use Kirby\Toolkit\Properties; */ abstract class Model { - use Properties; + use Properties; - /** - * Each model must define a CLASS_ALIAS - * which will be used in template queries. - * The CLASS_ALIAS is a short human-readable - * version of the class name. I.e. page. - */ - public const CLASS_ALIAS = null; + /** + * Each model must define a CLASS_ALIAS + * which will be used in template queries. + * The CLASS_ALIAS is a short human-readable + * version of the class name. I.e. page. + */ + public const CLASS_ALIAS = null; - /** - * The parent Kirby instance - * - * @var \Kirby\Cms\App - */ - public static $kirby; + /** + * The parent Kirby instance + * + * @var \Kirby\Cms\App + */ + public static $kirby; - /** - * The parent site instance - * - * @var \Kirby\Cms\Site - */ - protected $site; + /** + * The parent site instance + * + * @var \Kirby\Cms\Site + */ + protected $site; - /** - * Makes it possible to convert the entire model - * to a string. Mostly useful for debugging - * - * @return string - */ - public function __toString(): string - { - return $this->id(); - } + /** + * Makes it possible to convert the entire model + * to a string. Mostly useful for debugging + * + * @return string + */ + public function __toString(): string + { + return $this->id(); + } - /** - * Each model must return a unique id - * - * @return string|int - */ - public function id() - { - return null; - } + /** + * Each model must return a unique id + * + * @return string|int + */ + public function id() + { + return null; + } - /** - * Returns the parent Kirby instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return static::$kirby ??= App::instance(); - } + /** + * Returns the parent Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return static::$kirby ??= App::instance(); + } - /** - * Returns the parent Site instance - * - * @return \Kirby\Cms\Site - */ - public function site() - { - return $this->site ??= $this->kirby()->site(); - } + /** + * Returns the parent Site instance + * + * @return \Kirby\Cms\Site + */ + public function site() + { + return $this->site ??= $this->kirby()->site(); + } - /** - * Setter for the parent Kirby object - * - * @param \Kirby\Cms\App|null $kirby - * @return $this - */ - protected function setKirby(App $kirby = null) - { - static::$kirby = $kirby; - return $this; - } + /** + * Setter for the parent Kirby object + * + * @param \Kirby\Cms\App|null $kirby + * @return $this + */ + protected function setKirby(App $kirby = null) + { + static::$kirby = $kirby; + return $this; + } - /** - * Setter for the parent site object - * - * @internal - * @param \Kirby\Cms\Site|null $site - * @return $this - */ - public function setSite(Site $site = null) - { - $this->site = $site; - return $this; - } + /** + * Setter for the parent site object + * + * @internal + * @param \Kirby\Cms\Site|null $site + * @return $this + */ + public function setSite(Site $site = null) + { + $this->site = $site; + return $this; + } - /** - * Convert the model to a simple array - * - * @return array - */ - public function toArray(): array - { - return $this->propertiesToArray(); - } + /** + * Convert the model to a simple array + * + * @return array + */ + public function toArray(): array + { + return $this->propertiesToArray(); + } } diff --git a/kirby/src/Cms/ModelPermissions.php b/kirby/src/Cms/ModelPermissions.php index 86fcfbe..5212c4f 100644 --- a/kirby/src/Cms/ModelPermissions.php +++ b/kirby/src/Cms/ModelPermissions.php @@ -15,102 +15,102 @@ use Kirby\Toolkit\A; */ abstract class ModelPermissions { - protected $category; - protected $model; - protected $options; - protected $permissions; - protected $user; + protected $category; + protected $model; + protected $options; + protected $permissions; + protected $user; - /** - * @param string $method - * @param array $arguments - * @return bool - */ - public function __call(string $method, array $arguments = []): bool - { - return $this->can($method); - } + /** + * @param string $method + * @param array $arguments + * @return bool + */ + public function __call(string $method, array $arguments = []): bool + { + return $this->can($method); + } - /** - * ModelPermissions constructor - * - * @param \Kirby\Cms\Model $model - */ - public function __construct(Model $model) - { - $this->model = $model; - $this->options = $model->blueprint()->options(); - $this->user = $model->kirby()->user() ?? User::nobody(); - $this->permissions = $this->user->role()->permissions(); - } + /** + * ModelPermissions constructor + * + * @param \Kirby\Cms\Model $model + */ + public function __construct(Model $model) + { + $this->model = $model; + $this->options = $model->blueprint()->options(); + $this->user = $model->kirby()->user() ?? User::nobody(); + $this->permissions = $this->user->role()->permissions(); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * @param string $action - * @return bool - */ - public function can(string $action): bool - { - $role = $this->user->role()->id(); + /** + * @param string $action + * @return bool + */ + public function can(string $action): bool + { + $role = $this->user->role()->id(); - if ($role === 'nobody') { - return false; - } + if ($role === 'nobody') { + return false; + } - // check for a custom overall can method - if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) { - return false; - } + // check for a custom overall can method + if (method_exists($this, 'can' . $action) === true && $this->{'can' . $action}() === false) { + return false; + } - // evaluate the blueprint options block - if (isset($this->options[$action]) === true) { - $options = $this->options[$action]; + // evaluate the blueprint options block + if (isset($this->options[$action]) === true) { + $options = $this->options[$action]; - if ($options === false) { - return false; - } + if ($options === false) { + return false; + } - if ($options === true) { - return true; - } + if ($options === true) { + return true; + } - if (is_array($options) === true && A::isAssociative($options) === true) { - return $options[$role] ?? $options['*'] ?? false; - } - } + if (is_array($options) === true && A::isAssociative($options) === true) { + return $options[$role] ?? $options['*'] ?? false; + } + } - return $this->permissions->for($this->category, $action); - } + return $this->permissions->for($this->category, $action); + } - /** - * @param string $action - * @return bool - */ - public function cannot(string $action): bool - { - return $this->can($action) === false; - } + /** + * @param string $action + * @return bool + */ + public function cannot(string $action): bool + { + return $this->can($action) === false; + } - /** - * @return array - */ - public function toArray(): array - { - $array = []; + /** + * @return array + */ + public function toArray(): array + { + $array = []; - foreach ($this->options as $key => $value) { - $array[$key] = $this->can($key); - } + foreach ($this->options as $key => $value) { + $array[$key] = $this->can($key); + } - return $array; - } + return $array; + } } diff --git a/kirby/src/Cms/ModelWithContent.php b/kirby/src/Cms/ModelWithContent.php index 22c719b..2596ea1 100644 --- a/kirby/src/Cms/ModelWithContent.php +++ b/kirby/src/Cms/ModelWithContent.php @@ -20,680 +20,682 @@ use Throwable; */ abstract class ModelWithContent extends Model { - /** - * The content - * - * @var \Kirby\Cms\Content - */ - public $content; - - /** - * @var \Kirby\Cms\Translations - */ - public $translations; - - /** - * Returns the blueprint of the model - * - * @return \Kirby\Cms\Blueprint - */ - abstract public function blueprint(); - - /** - * Returns an array with all blueprints that are available - * - * @param string|null $inSection - * @return array - */ - public function blueprints(string $inSection = null): array - { - $blueprints = []; - $blueprint = $this->blueprint(); - $sections = $inSection !== null ? [$blueprint->section($inSection)] : $blueprint->sections(); - - foreach ($sections as $section) { - if ($section === null) { - continue; - } - - foreach ((array)$section->blueprints() as $blueprint) { - $blueprints[$blueprint['name']] = $blueprint; - } - } - - return array_values($blueprints); - } - - /** - * Executes any given model action - * - * @param string $action - * @param array $arguments - * @param \Closure $callback - * @return mixed - */ - abstract protected function commit(string $action, array $arguments, Closure $callback); - - /** - * Returns the content - * - * @param string|null $languageCode - * @return \Kirby\Cms\Content - * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist - */ - public function content(string $languageCode = null) - { - - // single language support - if ($this->kirby()->multilang() === false) { - if (is_a($this->content, 'Kirby\Cms\Content') === true) { - return $this->content; - } - - return $this->setContent($this->readContent())->content; - - // multi language support - } else { - - // only fetch from cache for the default language - if ($languageCode === null && is_a($this->content, 'Kirby\Cms\Content') === true) { - return $this->content; - } - - // get the translation by code - if ($translation = $this->translation($languageCode)) { - $content = new Content($translation->content(), $this); - } else { - throw new InvalidArgumentException('Invalid language: ' . $languageCode); - } - - // only store the content for the current language - if ($languageCode === null) { - $this->content = $content; - } - - return $content; - } - } - - /** - * Returns the absolute path to the content file - * - * @internal - * @param string|null $languageCode - * @param bool $force - * @return string - * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist - */ - public function contentFile(string $languageCode = null, bool $force = false): string - { - $extension = $this->contentFileExtension(); - $directory = $this->contentFileDirectory(); - $filename = $this->contentFileName(); - - // overwrite the language code - if ($force === true) { - if (empty($languageCode) === false) { - return $directory . '/' . $filename . '.' . $languageCode . '.' . $extension; - } else { - return $directory . '/' . $filename . '.' . $extension; - } - } - - // add and validate the language code in multi language mode - if ($this->kirby()->multilang() === true) { - if ($language = $this->kirby()->languageCode($languageCode)) { - return $directory . '/' . $filename . '.' . $language . '.' . $extension; - } else { - throw new InvalidArgumentException('Invalid language: ' . $languageCode); - } - } else { - return $directory . '/' . $filename . '.' . $extension; - } - } - - /** - * Returns an array with all content files - * - * @return array - */ - public function contentFiles(): array - { - if ($this->kirby()->multilang() === true) { - $files = []; - foreach ($this->kirby()->languages()->codes() as $code) { - $files[] = $this->contentFile($code); - } - return $files; - } else { - return [ - $this->contentFile() - ]; - } - } - - /** - * Prepares the content that should be written - * to the text file - * - * @internal - * @param array $data - * @param string|null $languageCode - * @return array - */ - public function contentFileData(array $data, string $languageCode = null): array - { - return $data; - } - - /** - * Returns the absolute path to the - * folder in which the content file is - * located - * - * @internal - * @return string|null - */ - public function contentFileDirectory(): ?string - { - return $this->root(); - } - - /** - * Returns the extension of the content file - * - * @internal - * @return string - */ - public function contentFileExtension(): string - { - return $this->kirby()->contentExtension(); - } - - /** - * Needs to be declared by the final model - * - * @internal - * @return string - */ - abstract public function contentFileName(): string; - - /** - * Decrement a given field value - * - * @param string $field - * @param int $by - * @param int $min - * @return static - */ - public function decrement(string $field, int $by = 1, int $min = 0) - { - $value = (int)$this->content()->get($field)->value() - $by; - - if ($value < $min) { - $value = $min; - } - - return $this->update([$field => $value]); - } - - /** - * Returns all content validation errors - * - * @return array - */ - public function errors(): array - { - $errors = []; - - foreach ($this->blueprint()->sections() as $section) { - $errors = array_merge($errors, $section->errors()); - } - - return $errors; - } - - /** - * Increment a given field value - * - * @param string $field - * @param int $by - * @param int|null $max - * @return static - */ - public function increment(string $field, int $by = 1, int $max = null) - { - $value = (int)$this->content()->get($field)->value() + $by; - - if ($max && $value > $max) { - $value = $max; - } - - return $this->update([$field => $value]); - } - - /** - * Checks if the model is locked for the current user - * - * @return bool - */ - public function isLocked(): bool - { - $lock = $this->lock(); - return $lock && $lock->isLocked() === true; - } - - /** - * Checks if the data has any errors - * - * @return bool - */ - public function isValid(): bool - { - return Form::for($this)->hasErrors() === false; - } - - /** - * Returns the lock object for this model - * - * Only if a content directory exists, - * virtual pages will need to overwrite this method - * - * @return \Kirby\Cms\ContentLock|null - */ - public function lock() - { - $dir = $this->contentFileDirectory(); - - if ( - $this->kirby()->option('content.locking', true) && - is_string($dir) === true && - file_exists($dir) === true - ) { - return new ContentLock($this); - } - } - - /** - * Returns the panel info of the model - * @since 3.6.0 - * - * @return \Kirby\Panel\Model - */ - abstract public function panel(); - - /** - * Must return the permissions object for the model - * - * @return \Kirby\Cms\ModelPermissions - */ - abstract public function permissions(); - - /** - * Creates a string query, starting from the model - * - * @internal - * @param string|null $query - * @param string|null $expect - * @return mixed - */ - public function query(string $query = null, string $expect = null) - { - if ($query === null) { - return null; - } - - try { - $result = Str::query($query, [ - 'kirby' => $this->kirby(), - 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), - static::CLASS_ALIAS => $this - ]); - } catch (Throwable $e) { - return null; - } - - if ($expect !== null && is_a($result, $expect) !== true) { - return null; - } - - return $result; - } - - /** - * Read the content from the content file - * - * @internal - * @param string|null $languageCode - * @return array - */ - public function readContent(string $languageCode = null): array - { - try { - return Data::read($this->contentFile($languageCode)); - } catch (Throwable $e) { - return []; - } - } - - /** - * Returns the absolute path to the model - * - * @return string|null - */ - abstract public function root(): ?string; - - /** - * Stores the content on disk - * - * @internal - * @param array|null $data - * @param string|null $languageCode - * @param bool $overwrite - * @return static - */ - public function save(array $data = null, string $languageCode = null, bool $overwrite = false) - { - if ($this->kirby()->multilang() === true) { - return $this->saveTranslation($data, $languageCode, $overwrite); - } else { - return $this->saveContent($data, $overwrite); - } - } - - /** - * Save the single language content - * - * @param array|null $data - * @param bool $overwrite - * @return static - */ - protected function saveContent(array $data = null, bool $overwrite = false) - { - // create a clone to avoid modifying the original - $clone = $this->clone(); - - // merge the new data with the existing content - $clone->content()->update($data, $overwrite); - - // send the full content array to the writer - $clone->writeContent($clone->content()->toArray()); - - return $clone; - } - - /** - * Save a translation - * - * @param array|null $data - * @param string|null $languageCode - * @param bool $overwrite - * @return static - * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist - */ - protected function saveTranslation(array $data = null, string $languageCode = null, bool $overwrite = false) - { - // create a clone to not touch the original - $clone = $this->clone(); - - // fetch the matching translation and update all the strings - $translation = $clone->translation($languageCode); - - if ($translation === null) { - throw new InvalidArgumentException('Invalid language: ' . $languageCode); - } - - // get the content to store - $content = $translation->update($data, $overwrite)->content(); - $kirby = $this->kirby(); - $languageCode = $kirby->languageCode($languageCode); - - // remove all untranslatable fields - if ($languageCode !== $kirby->defaultLanguage()->code()) { - foreach ($this->blueprint()->fields() as $field) { - if (($field['translate'] ?? true) === false) { - $content[$field['name']] = null; - } - } - - // merge the translation with the new data - $translation->update($content, true); - } - - // send the full translation array to the writer - $clone->writeContent($translation->content(), $languageCode); - - // reset the content object - $clone->content = null; - - // return the updated model - return $clone; - } - - /** - * Sets the Content object - * - * @param array|null $content - * @return $this - */ - protected function setContent(array $content = null) - { - if ($content !== null) { - $content = new Content($content, $this); - } - - $this->content = $content; - return $this; - } - - /** - * Create the translations collection from an array - * - * @param array|null $translations - * @return $this - */ - protected function setTranslations(array $translations = null) - { - if ($translations !== null) { - $this->translations = new Collection(); - - foreach ($translations as $props) { - $props['parent'] = $this; - $translation = new ContentTranslation($props); - $this->translations->data[$translation->code()] = $translation; - } - } - - return $this; - } - - /** - * String template builder with automatic HTML escaping - * @since 3.6.0 - * - * @param string|null $template Template string or `null` to use the model ID - * @param array $data - * @param string $fallback Fallback for tokens in the template that cannot be replaced - * @return string - */ - public function toSafeString(string $template = null, array $data = [], string $fallback = ''): string - { - return $this->toString($template, $data, $fallback, 'safeTemplate'); - } - - /** - * String template builder - * - * @param string|null $template Template string or `null` to use the model ID - * @param array $data - * @param string $fallback Fallback for tokens in the template that cannot be replaced - * @param string $handler For internal use - * @return string - */ - public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string - { - if ($template === null) { - return $this->id() ?? ''; - } - - if ($handler !== 'template' && $handler !== 'safeTemplate') { - throw new InvalidArgumentException('Invalid toString handler'); // @codeCoverageIgnore - } - - $result = Str::$handler($template, array_replace([ - 'kirby' => $this->kirby(), - 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), - static::CLASS_ALIAS => $this - ], $data), ['fallback' => $fallback]); - - return $result; - } - - /** - * Returns a single translation by language code - * If no code is specified the current translation is returned - * - * @param string|null $languageCode - * @return \Kirby\Cms\ContentTranslation|null - */ - public function translation(string $languageCode = null) - { - return $this->translations()->find($languageCode ?? $this->kirby()->language()->code()); - } - - /** - * Returns the translations collection - * - * @return \Kirby\Cms\Collection - */ - public function translations() - { - if ($this->translations !== null) { - return $this->translations; - } - - $this->translations = new Collection(); - - foreach ($this->kirby()->languages() as $language) { - $translation = new ContentTranslation([ - 'parent' => $this, - 'code' => $language->code(), - ]); - - $this->translations->data[$translation->code()] = $translation; - } - - return $this->translations; - } - - /** - * Updates the model data - * - * @param array|null $input - * @param string|null $languageCode - * @param bool $validate - * @return static - * @throws \Kirby\Exception\InvalidArgumentException If the input array contains invalid values - */ - public function update(array $input = null, string $languageCode = null, bool $validate = false) - { - $form = Form::for($this, [ - 'ignoreDisabled' => $validate === false, - 'input' => $input, - 'language' => $languageCode, - ]); - - // validate the input - if ($validate === true) { - if ($form->isInvalid() === true) { - throw new InvalidArgumentException([ - 'fallback' => 'Invalid form with errors', - 'details' => $form->errors() - ]); - } - } - - $arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode]; - return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) { - // save updated values - $model = $model->save($strings, $languageCode, true); - - // update model in siblings collection - $model->siblings()->add($model); - - return $model; - }); - } - - /** - * Low level data writer method - * to store the given data on disk or anywhere else - * - * @internal - * @param array $data - * @param string|null $languageCode - * @return bool - */ - public function writeContent(array $data, string $languageCode = null): bool - { - return Data::write( - $this->contentFile($languageCode), - $this->contentFileData($data, $languageCode) - ); - } - - - /** - * Deprecated! - */ - - /** - * Returns the panel icon definition - * - * @deprecated 3.6.0 Use `->panel()->image()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param array|null $params - * @return array|null - * @codeCoverageIgnore - */ - public function panelIcon(array $params = null): ?array - { - return $this->panel()->image($params); - } - - /** - * @deprecated 3.6.0 Use `->panel()->image()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param string|array|false|null $settings - * @return array|null - * @codeCoverageIgnore - */ - public function panelImage($settings = null): ?array - { - return $this->panel()->image($settings); - } - - /** - * Returns an array of all actions - * that can be performed in the Panel - * This also checks for the lock status - * - * @deprecated 3.6.0 Use `->panel()->options()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @param array $unlock An array of options that will be force-unlocked - * @return array - * @codeCoverageIgnore - */ - public function panelOptions(array $unlock = []): array - { - return $this->panel()->options($unlock); - } + /** + * The content + * + * @var \Kirby\Cms\Content + */ + public $content; + + /** + * @var \Kirby\Cms\Translations + */ + public $translations; + + /** + * Returns the blueprint of the model + * + * @return \Kirby\Cms\Blueprint + */ + abstract public function blueprint(); + + /** + * Returns an array with all blueprints that are available + * + * @param string|null $inSection + * @return array + */ + public function blueprints(string $inSection = null): array + { + $blueprints = []; + $blueprint = $this->blueprint(); + $sections = $inSection !== null ? [$blueprint->section($inSection)] : $blueprint->sections(); + + foreach ($sections as $section) { + if ($section === null) { + continue; + } + + foreach ((array)$section->blueprints() as $blueprint) { + $blueprints[$blueprint['name']] = $blueprint; + } + } + + return array_values($blueprints); + } + + /** + * Executes any given model action + * + * @param string $action + * @param array $arguments + * @param \Closure $callback + * @return mixed + */ + abstract protected function commit(string $action, array $arguments, Closure $callback); + + /** + * Returns the content + * + * @param string|null $languageCode + * @return \Kirby\Cms\Content + * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist + */ + public function content(string $languageCode = null) + { + // single language support + if ($this->kirby()->multilang() === false) { + if (is_a($this->content, 'Kirby\Cms\Content') === true) { + return $this->content; + } + + // don't normalize field keys (already handled by the `Data` class) + return $this->content = new Content($this->readContent(), $this, false); + + // multi language support + } else { + // only fetch from cache for the default language + if ($languageCode === null && is_a($this->content, 'Kirby\Cms\Content') === true) { + return $this->content; + } + + // get the translation by code + if ($translation = $this->translation($languageCode)) { + // don't normalize field keys (already handled by the `ContentTranslation` class) + $content = new Content($translation->content(), $this, false); + } else { + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + + // only store the content for the current language + if ($languageCode === null) { + $this->content = $content; + } + + return $content; + } + } + + /** + * Returns the absolute path to the content file + * + * @internal + * @param string|null $languageCode + * @param bool $force + * @return string + * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist + */ + public function contentFile(string $languageCode = null, bool $force = false): string + { + $extension = $this->contentFileExtension(); + $directory = $this->contentFileDirectory(); + $filename = $this->contentFileName(); + + // overwrite the language code + if ($force === true) { + if (empty($languageCode) === false) { + return $directory . '/' . $filename . '.' . $languageCode . '.' . $extension; + } else { + return $directory . '/' . $filename . '.' . $extension; + } + } + + // add and validate the language code in multi language mode + if ($this->kirby()->multilang() === true) { + if ($language = $this->kirby()->languageCode($languageCode)) { + return $directory . '/' . $filename . '.' . $language . '.' . $extension; + } else { + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + } else { + return $directory . '/' . $filename . '.' . $extension; + } + } + + /** + * Returns an array with all content files + * + * @return array + */ + public function contentFiles(): array + { + if ($this->kirby()->multilang() === true) { + $files = []; + foreach ($this->kirby()->languages()->codes() as $code) { + $files[] = $this->contentFile($code); + } + return $files; + } else { + return [ + $this->contentFile() + ]; + } + } + + /** + * Prepares the content that should be written + * to the text file + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + return $data; + } + + /** + * Returns the absolute path to the + * folder in which the content file is + * located + * + * @internal + * @return string|null + */ + public function contentFileDirectory(): ?string + { + return $this->root(); + } + + /** + * Returns the extension of the content file + * + * @internal + * @return string + */ + public function contentFileExtension(): string + { + return $this->kirby()->contentExtension(); + } + + /** + * Needs to be declared by the final model + * + * @internal + * @return string + */ + abstract public function contentFileName(): string; + + /** + * Decrement a given field value + * + * @param string $field + * @param int $by + * @param int $min + * @return static + */ + public function decrement(string $field, int $by = 1, int $min = 0) + { + $value = (int)$this->content()->get($field)->value() - $by; + + if ($value < $min) { + $value = $min; + } + + return $this->update([$field => $value]); + } + + /** + * Returns all content validation errors + * + * @return array + */ + public function errors(): array + { + $errors = []; + + foreach ($this->blueprint()->sections() as $section) { + $errors = array_merge($errors, $section->errors()); + } + + return $errors; + } + + /** + * Increment a given field value + * + * @param string $field + * @param int $by + * @param int|null $max + * @return static + */ + public function increment(string $field, int $by = 1, int $max = null) + { + $value = (int)$this->content()->get($field)->value() + $by; + + if ($max && $value > $max) { + $value = $max; + } + + return $this->update([$field => $value]); + } + + /** + * Checks if the model is locked for the current user + * + * @return bool + */ + public function isLocked(): bool + { + $lock = $this->lock(); + return $lock && $lock->isLocked() === true; + } + + /** + * Checks if the data has any errors + * + * @return bool + */ + public function isValid(): bool + { + return Form::for($this)->hasErrors() === false; + } + + /** + * Returns the lock object for this model + * + * Only if a content directory exists, + * virtual pages will need to overwrite this method + * + * @return \Kirby\Cms\ContentLock|null + */ + public function lock() + { + $dir = $this->contentFileDirectory(); + + if ( + $this->kirby()->option('content.locking', true) && + is_string($dir) === true && + file_exists($dir) === true + ) { + return new ContentLock($this); + } + } + + /** + * Returns the panel info of the model + * @since 3.6.0 + * + * @return \Kirby\Panel\Model + */ + abstract public function panel(); + + /** + * Must return the permissions object for the model + * + * @return \Kirby\Cms\ModelPermissions + */ + abstract public function permissions(); + + /** + * Creates a string query, starting from the model + * + * @internal + * @param string|null $query + * @param string|null $expect + * @return mixed + */ + public function query(string $query = null, string $expect = null) + { + if ($query === null) { + return null; + } + + try { + $result = Str::query($query, [ + 'kirby' => $this->kirby(), + 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), + 'model' => $this, + static::CLASS_ALIAS => $this + ]); + } catch (Throwable $e) { + return null; + } + + if ($expect !== null && is_a($result, $expect) !== true) { + return null; + } + + return $result; + } + + /** + * Read the content from the content file + * + * @internal + * @param string|null $languageCode + * @return array + */ + public function readContent(string $languageCode = null): array + { + try { + return Data::read($this->contentFile($languageCode)); + } catch (Throwable $e) { + return []; + } + } + + /** + * Returns the absolute path to the model + * + * @return string|null + */ + abstract public function root(): ?string; + + /** + * Stores the content on disk + * + * @internal + * @param array|null $data + * @param string|null $languageCode + * @param bool $overwrite + * @return static + */ + public function save(array $data = null, string $languageCode = null, bool $overwrite = false) + { + if ($this->kirby()->multilang() === true) { + return $this->saveTranslation($data, $languageCode, $overwrite); + } else { + return $this->saveContent($data, $overwrite); + } + } + + /** + * Save the single language content + * + * @param array|null $data + * @param bool $overwrite + * @return static + */ + protected function saveContent(array $data = null, bool $overwrite = false) + { + // create a clone to avoid modifying the original + $clone = $this->clone(); + + // merge the new data with the existing content + $clone->content()->update($data, $overwrite); + + // send the full content array to the writer + $clone->writeContent($clone->content()->toArray()); + + return $clone; + } + + /** + * Save a translation + * + * @param array|null $data + * @param string|null $languageCode + * @param bool $overwrite + * @return static + * @throws \Kirby\Exception\InvalidArgumentException If the language for the given code does not exist + */ + protected function saveTranslation(array $data = null, string $languageCode = null, bool $overwrite = false) + { + // create a clone to not touch the original + $clone = $this->clone(); + + // fetch the matching translation and update all the strings + $translation = $clone->translation($languageCode); + + if ($translation === null) { + throw new InvalidArgumentException('Invalid language: ' . $languageCode); + } + + // get the content to store + $content = $translation->update($data, $overwrite)->content(); + $kirby = $this->kirby(); + $languageCode = $kirby->languageCode($languageCode); + + // remove all untranslatable fields + if ($languageCode !== $kirby->defaultLanguage()->code()) { + foreach ($this->blueprint()->fields() as $field) { + if (($field['translate'] ?? true) === false) { + $content[strtolower($field['name'])] = null; + } + } + + // merge the translation with the new data + $translation->update($content, true); + } + + // send the full translation array to the writer + $clone->writeContent($translation->content(), $languageCode); + + // reset the content object + $clone->content = null; + + // return the updated model + return $clone; + } + + /** + * Sets the Content object + * + * @param array|null $content + * @return $this + */ + protected function setContent(array $content = null) + { + if ($content !== null) { + $content = new Content($content, $this); + } + + $this->content = $content; + return $this; + } + + /** + * Create the translations collection from an array + * + * @param array|null $translations + * @return $this + */ + protected function setTranslations(array $translations = null) + { + if ($translations !== null) { + $this->translations = new Collection(); + + foreach ($translations as $props) { + $props['parent'] = $this; + $translation = new ContentTranslation($props); + $this->translations->data[$translation->code()] = $translation; + } + } + + return $this; + } + + /** + * String template builder with automatic HTML escaping + * @since 3.6.0 + * + * @param string|null $template Template string or `null` to use the model ID + * @param array $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @return string + */ + public function toSafeString(string $template = null, array $data = [], string $fallback = ''): string + { + return $this->toString($template, $data, $fallback, 'safeTemplate'); + } + + /** + * String template builder + * + * @param string|null $template Template string or `null` to use the model ID + * @param array $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @param string $handler For internal use + * @return string + */ + public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string + { + if ($template === null) { + return $this->id() ?? ''; + } + + if ($handler !== 'template' && $handler !== 'safeTemplate') { + throw new InvalidArgumentException('Invalid toString handler'); // @codeCoverageIgnore + } + + $result = Str::$handler($template, array_replace([ + 'kirby' => $this->kirby(), + 'site' => is_a($this, 'Kirby\Cms\Site') ? $this : $this->site(), + 'model' => $this, + static::CLASS_ALIAS => $this, + ], $data), ['fallback' => $fallback]); + + return $result; + } + + /** + * Returns a single translation by language code + * If no code is specified the current translation is returned + * + * @param string|null $languageCode + * @return \Kirby\Cms\ContentTranslation|null + */ + public function translation(string $languageCode = null) + { + return $this->translations()->find($languageCode ?? $this->kirby()->language()->code()); + } + + /** + * Returns the translations collection + * + * @return \Kirby\Cms\Collection + */ + public function translations() + { + if ($this->translations !== null) { + return $this->translations; + } + + $this->translations = new Collection(); + + foreach ($this->kirby()->languages() as $language) { + $translation = new ContentTranslation([ + 'parent' => $this, + 'code' => $language->code(), + ]); + + $this->translations->data[$translation->code()] = $translation; + } + + return $this->translations; + } + + /** + * Updates the model data + * + * @param array|null $input + * @param string|null $languageCode + * @param bool $validate + * @return static + * @throws \Kirby\Exception\InvalidArgumentException If the input array contains invalid values + */ + public function update(array $input = null, string $languageCode = null, bool $validate = false) + { + $form = Form::for($this, [ + 'ignoreDisabled' => $validate === false, + 'input' => $input, + 'language' => $languageCode, + ]); + + // validate the input + if ($validate === true) { + if ($form->isInvalid() === true) { + throw new InvalidArgumentException([ + 'fallback' => 'Invalid form with errors', + 'details' => $form->errors() + ]); + } + } + + $arguments = [static::CLASS_ALIAS => $this, 'values' => $form->data(), 'strings' => $form->strings(), 'languageCode' => $languageCode]; + return $this->commit('update', $arguments, function ($model, $values, $strings, $languageCode) { + // save updated values + $model = $model->save($strings, $languageCode, true); + + // update model in siblings collection + $model->siblings()->add($model); + + return $model; + }); + } + + /** + * Low level data writer method + * to store the given data on disk or anywhere else + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return bool + */ + public function writeContent(array $data, string $languageCode = null): bool + { + return Data::write( + $this->contentFile($languageCode), + $this->contentFileData($data, $languageCode) + ); + } + + + /** + * Deprecated! + */ + + /** + * Returns the panel icon definition + * + * @deprecated 3.6.0 Use `->panel()->image()` instead + * @todo Remove in 3.8.0 + * + * @internal + * @param array|null $params + * @return array|null + * @codeCoverageIgnore + */ + public function panelIcon(array $params = null): ?array + { + Helpers::deprecated('Cms\ModelWithContent::panelIcon() has been deprecated and will be removed in Kirby 3.8.0. Use $model->panel()->image() instead.'); + return $this->panel()->image($params); + } + + /** + * @deprecated 3.6.0 Use `->panel()->image()` instead + * @todo Remove in 3.8.0 + * + * @internal + * @param string|array|false|null $settings + * @return array|null + * @codeCoverageIgnore + */ + public function panelImage($settings = null): ?array + { + Helpers::deprecated('Cms\ModelWithContent::panelImage() has been deprecated and will be removed in Kirby 3.8.0. Use $model->panel()->image() instead.'); + return $this->panel()->image($settings); + } + + /** + * Returns an array of all actions + * that can be performed in the Panel + * This also checks for the lock status + * + * @deprecated 3.6.0 Use `->panel()->options()` instead + * @todo Remove in 3.8.0 + * + * @param array $unlock An array of options that will be force-unlocked + * @return array + * @codeCoverageIgnore + */ + public function panelOptions(array $unlock = []): array + { + Helpers::deprecated('Cms\ModelWithContent::panelOptions() has been deprecated and will be removed in Kirby 3.8.0. Use $model->panel()->options() instead.'); + return $this->panel()->options($unlock); + } } diff --git a/kirby/src/Cms/Nest.php b/kirby/src/Cms/Nest.php index bbaf810..56455b3 100644 --- a/kirby/src/Cms/Nest.php +++ b/kirby/src/Cms/Nest.php @@ -18,31 +18,31 @@ namespace Kirby\Cms; */ class Nest { - /** - * @param $data - * @param null $parent - * @return mixed - */ - public static function create($data, $parent = null) - { - if (is_scalar($data) === true) { - return new Field($parent, $data, $data); - } + /** + * @param $data + * @param null $parent + * @return mixed + */ + public static function create($data, $parent = null) + { + if (is_scalar($data) === true) { + return new Field($parent, $data, $data); + } - $result = []; + $result = []; - foreach ($data as $key => $value) { - if (is_array($value) === true) { - $result[$key] = static::create($value, $parent); - } elseif (is_scalar($value) === true) { - $result[$key] = new Field($parent, $key, $value); - } - } + foreach ($data as $key => $value) { + if (is_array($value) === true) { + $result[$key] = static::create($value, $parent); + } elseif (is_scalar($value) === true) { + $result[$key] = new Field($parent, $key, $value); + } + } - if (is_int(key($data))) { - return new NestCollection($result); - } else { - return new NestObject($result); - } - } + if (is_int(key($data))) { + return new NestCollection($result); + } else { + return new NestObject($result); + } + } } diff --git a/kirby/src/Cms/NestCollection.php b/kirby/src/Cms/NestCollection.php index e3857b0..b5b1a7b 100644 --- a/kirby/src/Cms/NestCollection.php +++ b/kirby/src/Cms/NestCollection.php @@ -16,16 +16,16 @@ use Kirby\Toolkit\Collection as BaseCollection; */ class NestCollection extends BaseCollection { - /** - * Converts all objects in the collection - * to an array. This can also take a callback - * function to further modify the array result. - * - * @param \Closure|null $map - * @return array - */ - public function toArray(Closure $map = null): array - { - return parent::toArray($map ?? fn ($object) => $object->toArray()); - } + /** + * Converts all objects in the collection + * to an array. This can also take a callback + * function to further modify the array result. + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + return parent::toArray($map ?? fn ($object) => $object->toArray()); + } } diff --git a/kirby/src/Cms/NestObject.php b/kirby/src/Cms/NestObject.php index 026be2c..4744539 100644 --- a/kirby/src/Cms/NestObject.php +++ b/kirby/src/Cms/NestObject.php @@ -15,29 +15,29 @@ use Kirby\Toolkit\Obj; */ class NestObject extends Obj { - /** - * Converts the object to an array - * - * @return array - */ - public function toArray(): array - { - $result = []; + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + $result = []; - foreach ((array)$this as $key => $value) { - if (is_a($value, 'Kirby\Cms\Field') === true) { - $result[$key] = $value->value(); - continue; - } + foreach ((array)$this as $key => $value) { + if (is_a($value, 'Kirby\Cms\Field') === true) { + $result[$key] = $value->value(); + continue; + } - if (is_object($value) === true && method_exists($value, 'toArray')) { - $result[$key] = $value->toArray(); - continue; - } + if (is_object($value) === true && method_exists($value, 'toArray')) { + $result[$key] = $value->toArray(); + continue; + } - $result[$key] = $value; - } + $result[$key] = $value; + } - return $result; - } + return $result; + } } diff --git a/kirby/src/Cms/Page.php b/kirby/src/Cms/Page.php index c583d97..88c95cd 100644 --- a/kirby/src/Cms/Page.php +++ b/kirby/src/Cms/Page.php @@ -7,6 +7,7 @@ use Kirby\Exception\InvalidArgumentException; use Kirby\Exception\NotFoundException; use Kirby\Filesystem\Dir; use Kirby\Filesystem\F; +use Kirby\Http\Response; use Kirby\Http\Uri; use Kirby\Panel\Page as Panel; use Kirby\Toolkit\A; @@ -25,1530 +26,1538 @@ use Kirby\Toolkit\A; */ class Page extends ModelWithContent { - use PageActions; - use PageSiblings; - use HasChildren; - use HasFiles; - use HasMethods; - use HasSiblings; - - public const CLASS_ALIAS = 'page'; - - /** - * All registered page methods - * - * @var array - */ - public static $methods = []; - - /** - * Registry with all Page models - * - * @var array - */ - public static $models = []; - - /** - * The PageBlueprint object - * - * @var \Kirby\Cms\PageBlueprint - */ - protected $blueprint; - - /** - * Nesting level - * - * @var int - */ - protected $depth; - - /** - * Sorting number + slug - * - * @var string - */ - protected $dirname; - - /** - * Path of dirnames - * - * @var string - */ - protected $diruri; - - /** - * Draft status flag - * - * @var bool - */ - protected $isDraft; - - /** - * The Page id - * - * @var string - */ - protected $id; - - /** - * The template, that should be loaded - * if it exists - * - * @var \Kirby\Cms\Template - */ - protected $intendedTemplate; - - /** - * @var array - */ - protected $inventory; - - /** - * The sorting number - * - * @var int|null - */ - protected $num; - - /** - * The parent page - * - * @var \Kirby\Cms\Page|null - */ - protected $parent; - - /** - * Absolute path to the page directory - * - * @var string - */ - protected $root; - - /** - * The parent Site object - * - * @var \Kirby\Cms\Site|null - */ - protected $site; - - /** - * The URL-appendix aka slug - * - * @var string - */ - protected $slug; - - /** - * The intended page template - * - * @var \Kirby\Cms\Template - */ - protected $template; - - /** - * The page url - * - * @var string|null - */ - protected $url; - - /** - * Magic caller - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } - - // page methods - if ($this->hasMethod($method)) { - return $this->callMethod($method, $arguments); - } - - // return page content otherwise - return $this->content()->get($method); - } - - /** - * Creates a new page object - * - * @param array $props - */ - public function __construct(array $props) - { - // set the slug as the first property - $this->slug = $props['slug'] ?? null; - - // add all other properties - $this->setProperties($props); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return array_merge($this->toArray(), [ - 'content' => $this->content(), - 'children' => $this->children(), - 'siblings' => $this->siblings(), - 'translations' => $this->translations(), - 'files' => $this->files(), - ]); - } - - /** - * Returns the url to the api endpoint - * - * @internal - * @param bool $relative - * @return string - */ - public function apiUrl(bool $relative = false): string - { - if ($relative === true) { - return 'pages/' . $this->panel()->id(); - } else { - return $this->kirby()->url('api') . '/pages/' . $this->panel()->id(); - } - } - - /** - * Returns the blueprint object - * - * @return \Kirby\Cms\PageBlueprint - */ - public function blueprint() - { - if (is_a($this->blueprint, 'Kirby\Cms\PageBlueprint') === true) { - return $this->blueprint; - } - - return $this->blueprint = PageBlueprint::factory('pages/' . $this->intendedTemplate(), 'pages/default', $this); - } - - /** - * Returns an array with all blueprints that are available for the page - * - * @param string|null $inSection - * @return array - */ - public function blueprints(?string $inSection = null): array - { - if ($inSection !== null) { - return $this->blueprint()->section($inSection)->blueprints(); - } - - $blueprints = []; - $templates = $this->blueprint()->changeTemplate() ?? $this->blueprint()->options()['changeTemplate'] ?? []; - $currentTemplate = $this->intendedTemplate()->name(); - - if (is_array($templates) === false) { - $templates = []; - } - - // add the current template to the array if it's not already there - if (in_array($currentTemplate, $templates) === false) { - array_unshift($templates, $currentTemplate); - } - - // make sure every template is only included once - $templates = array_unique($templates); - - foreach ($templates as $template) { - try { - $props = Blueprint::load('pages/' . $template); - - $blueprints[] = [ - 'name' => basename($props['name']), - 'title' => $props['title'], - ]; - } catch (Exception $e) { - // skip invalid blueprints - } - } - - return array_values($blueprints); - } - - /** - * Builds the cache id for the page - * - * @param string $contentType - * @return string - */ - protected function cacheId(string $contentType): string - { - $cacheId = [$this->id()]; - - if ($this->kirby()->multilang() === true) { - $cacheId[] = $this->kirby()->language()->code(); - } - - $cacheId[] = $contentType; - - return implode('.', $cacheId); - } - - /** - * Prepares the content for the write method - * - * @internal - * @param array $data - * @param string|null $languageCode - * @return array - */ - public function contentFileData(array $data, ?string $languageCode = null): array - { - return A::prepend($data, [ - 'title' => $data['title'] ?? null, - 'slug' => $data['slug'] ?? null - ]); - } - - /** - * Returns the content text file - * which is found by the inventory method - * - * @internal - * @param string|null $languageCode - * @return string - */ - public function contentFileName(?string $languageCode = null): string - { - return $this->intendedTemplate()->name(); - } - - /** - * Call the page controller - * - * @internal - * @param array $data - * @param string $contentType - * @return array - * @throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby`, `site`, `pages` or `page` - */ - public function controller(array $data = [], string $contentType = 'html'): array - { - // create the template data - $data = array_merge($data, [ - 'kirby' => $kirby = $this->kirby(), - 'site' => $site = $this->site(), - 'pages' => $site->children(), - 'page' => $site->visit($this) - ]); - - // call the template controller if there's one. - $controllerData = $kirby->controller($this->template()->name(), $data, $contentType); - - // merge controller data with original data safely - if (empty($controllerData) === false) { - $classes = [ - 'kirby' => 'Kirby\Cms\App', - 'site' => 'Kirby\Cms\Site', - 'pages' => 'Kirby\Cms\Pages', - 'page' => 'Kirby\Cms\Page' - ]; - - foreach ($controllerData as $key => $value) { - if (array_key_exists($key, $classes) === true) { - if (is_a($value, $classes[$key]) === true) { - $data[$key] = $value; - } else { - throw new InvalidArgumentException('The returned variable "' . $key . '" from the controller "' . $this->template()->name() . '" is not of the required type "' . $classes[$key] . '"'); - } - } else { - $data[$key] = $value; - } - } - } - - return $data; - } - - /** - * Returns a number indicating how deep the page - * is nested within the content folder - * - * @return int - */ - public function depth(): int - { - return $this->depth ??= (substr_count($this->id(), '/') + 1); - } - - /** - * Sorting number + Slug - * - * @return string - */ - public function dirname(): string - { - if ($this->dirname !== null) { - return $this->dirname; - } - - if ($this->num() !== null) { - return $this->dirname = $this->num() . Dir::$numSeparator . $this->uid(); - } else { - return $this->dirname = $this->uid(); - } - } - - /** - * Sorting number + Slug - * - * @return string - */ - public function diruri(): string - { - if (is_string($this->diruri) === true) { - return $this->diruri; - } - - if ($this->isDraft() === true) { - $dirname = '_drafts/' . $this->dirname(); - } else { - $dirname = $this->dirname(); - } - - if ($parent = $this->parent()) { - return $this->diruri = $parent->diruri() . '/' . $dirname; - } else { - return $this->diruri = $dirname; - } - } - - /** - * Checks if the page exists on disk - * - * @return bool - */ - public function exists(): bool - { - return is_dir($this->root()) === true; - } - - /** - * Constructs a Page object and also - * takes page models into account. - * - * @internal - * @param mixed $props - * @return static - */ - public static function factory($props) - { - if (empty($props['model']) === false) { - return static::model($props['model'], $props); - } - - return new static($props); - } - - /** - * Redirects to this page, - * wrapper for the `go()` helper - * - * @since 3.4.0 - * - * @param array $options Options for `Kirby\Http\Uri` to create URL parts - * @param int $code HTTP status code - */ - public function go(array $options = [], int $code = 302) - { - go($this->url($options), $code); - } - - /** - * Checks if the intended template - * for the page exists. - * - * @return bool - */ - public function hasTemplate(): bool - { - return $this->intendedTemplate() === $this->template(); - } - - /** - * Returns the Page Id - * - * @return string - */ - public function id(): string - { - if ($this->id !== null) { - return $this->id; - } - - // set the id, depending on the parent - if ($parent = $this->parent()) { - return $this->id = $parent->id() . '/' . $this->uid(); - } - - return $this->id = $this->uid(); - } - - /** - * Returns the template that should be - * loaded if it exists. - * - * @return \Kirby\Cms\Template - */ - public function intendedTemplate() - { - if ($this->intendedTemplate !== null) { - return $this->intendedTemplate; - } - - return $this->setTemplate($this->inventory()['template'])->intendedTemplate(); - } - - /** - * Returns the inventory of files - * children and content files - * - * @internal - * @return array - */ - public function inventory(): array - { - if ($this->inventory !== null) { - return $this->inventory; - } - - $kirby = $this->kirby(); - - return $this->inventory = Dir::inventory( - $this->root(), - $kirby->contentExtension(), - $kirby->contentIgnore(), - $kirby->multilang() - ); - } - - /** - * Compares the current object with the given page object - * - * @param \Kirby\Cms\Page|string $page - * @return bool - */ - public function is($page): bool - { - if (is_a($page, 'Kirby\Cms\Page') === false) { - if (is_string($page) === false) { - return false; - } - - $page = $this->kirby()->page($page); - } - - if (is_a($page, 'Kirby\Cms\Page') === false) { - return false; - } - - return $this->id() === $page->id(); - } - - /** - * Checks if the page is the current page - * - * @return bool - */ - public function isActive(): bool - { - if ($page = $this->site()->page()) { - if ($page->is($this) === true) { - return true; - } - } - - return false; - } - - /** - * Checks if the page is a direct or indirect ancestor of the given $page object - * - * @param Page $child - * @return bool - */ - public function isAncestorOf(Page $child): bool - { - return $child->parents()->has($this->id()) === true; - } - - /** - * Checks if the page can be cached in the - * pages cache. This will also check if one - * of the ignore rules from the config kick in. - * - * @return bool - */ - public function isCacheable(): bool - { - $kirby = $this->kirby(); - $cache = $kirby->cache('pages'); - $options = $cache->options(); - $ignore = $options['ignore'] ?? null; - - // the pages cache is switched off - if (($options['active'] ?? false) === false) { - return false; - } - - // inspect the current request - $request = $kirby->request(); - - // disable the pages cache for any request types but GET or HEAD - if (in_array($request->method(), ['GET', 'HEAD']) === false) { - return false; - } - - // disable the pages cache when there's request data - if (empty($request->data()) === false) { - return false; - } - - // disable the pages cache when there are any params - if ($request->params()->isNotEmpty()) { - return false; - } - - // check for a custom ignore rule - if (is_a($ignore, 'Closure') === true) { - if ($ignore($this) === true) { - return false; - } - } - - // ignore pages by id - if (is_array($ignore) === true) { - if (in_array($this->id(), $ignore) === true) { - return false; - } - } - - return true; - } - - /** - * Checks if the page is a child of the given page - * - * @param \Kirby\Cms\Page|string $parent - * @return bool - */ - public function isChildOf($parent): bool - { - if ($parentObj = $this->parent()) { - return $parentObj->is($parent); - } - - return false; - } - - /** - * Checks if the page is a descendant of the given page - * - * @param \Kirby\Cms\Page|string $parent - * @return bool - */ - public function isDescendantOf($parent): bool - { - if (is_string($parent) === true) { - $parent = $this->site()->find($parent); - } - - if (!$parent) { - return false; - } - - return $this->parents()->has($parent->id()) === true; - } - - /** - * Checks if the page is a descendant of the currently active page - * - * @return bool - */ - public function isDescendantOfActive(): bool - { - if ($active = $this->site()->page()) { - return $this->isDescendantOf($active); - } - - return false; - } - - /** - * Checks if the current page is a draft - * - * @return bool - */ - public function isDraft(): bool - { - return $this->isDraft; - } - - /** - * Checks if the page is the error page - * - * @return bool - */ - public function isErrorPage(): bool - { - return $this->id() === $this->site()->errorPageId(); - } - - /** - * Checks if the page is the home page - * - * @return bool - */ - public function isHomePage(): bool - { - return $this->id() === $this->site()->homePageId(); - } - - /** - * It's often required to check for the - * home and error page to stop certain - * actions. That's why there's a shortcut. - * - * @return bool - */ - public function isHomeOrErrorPage(): bool - { - return $this->isHomePage() === true || $this->isErrorPage() === true; - } - - /** - * Checks if the page has a sorting number - * - * @return bool - */ - public function isListed(): bool - { - return $this->num() !== null; - } - - /** - * Checks if the page is open. - * Open pages are either the current one - * or descendants of the current one. - * - * @return bool - */ - public function isOpen(): bool - { - if ($this->isActive() === true) { - return true; - } - - if ($page = $this->site()->page()) { - if ($page->parents()->has($this->id()) === true) { - return true; - } - } - - return false; - } - - /** - * Checks if the page is not a draft. - * - * @return bool - */ - public function isPublished(): bool - { - return $this->isDraft() === false; - } - - /** - * Check if the page can be read by the current user - * - * @return bool - */ - public function isReadable(): bool - { - static $readable = []; - - $template = $this->intendedTemplate()->name(); - - if (isset($readable[$template]) === true) { - return $readable[$template]; - } - - return $readable[$template] = $this->permissions()->can('read'); - } - - /** - * Checks if the page is sortable - * - * @return bool - */ - public function isSortable(): bool - { - return $this->permissions()->can('sort'); - } - - /** - * Checks if the page has no sorting number - * - * @return bool - */ - public function isUnlisted(): bool - { - return $this->isListed() === false; - } - - /** - * Checks if the page access is verified. - * This is only used for drafts so far. - * - * @internal - * @param string|null $token - * @return bool - */ - public function isVerified(string $token = null) - { - if ( - $this->isDraft() === false && - $this->parents()->findBy('status', 'draft') === null - ) { - return true; - } - - if ($token === null) { - return false; - } - - return $this->token() === $token; - } - - /** - * Returns the root to the media folder for the page - * - * @internal - * @return string - */ - public function mediaRoot(): string - { - return $this->kirby()->root('media') . '/pages/' . $this->id(); - } - - /** - * The page's base URL for any files - * - * @internal - * @return string - */ - public function mediaUrl(): string - { - return $this->kirby()->url('media') . '/pages/' . $this->id(); - } - - /** - * Creates a page model if it has been registered - * - * @internal - * @param string $name - * @param array $props - * @return static - */ - public static function model(string $name, array $props = []) - { - if ($class = (static::$models[$name] ?? null)) { - $object = new $class($props); - - if (is_a($object, 'Kirby\Cms\Page') === true) { - return $object; - } - } - - return new static($props); - } - - /** - * Returns the last modification date of the page - * - * @param string|null $format - * @param string|null $handler - * @param string|null $languageCode - * @return int|string - */ - public function modified(string $format = null, string $handler = null, string $languageCode = null) - { - return F::modified( - $this->contentFile($languageCode), - $format, - $handler ?? $this->kirby()->option('date.handler', 'date') - ); - } - - /** - * Returns the sorting number - * - * @return int|null - */ - public function num(): ?int - { - return $this->num; - } - - /** - * Returns the panel info object - * - * @return \Kirby\Panel\Page - */ - public function panel() - { - return new Panel($this); - } - - /** - * Returns the parent Page object - * - * @return \Kirby\Cms\Page|null - */ - public function parent() - { - return $this->parent; - } - - /** - * Returns the parent id, if a parent exists - * - * @internal - * @return string|null - */ - public function parentId(): ?string - { - if ($parent = $this->parent()) { - return $parent->id(); - } - - return null; - } - - /** - * Returns the parent model, - * which can either be another Page - * or the Site - * - * @internal - * @return \Kirby\Cms\Page|\Kirby\Cms\Site - */ - public function parentModel() - { - return $this->parent() ?? $this->site(); - } - - /** - * Returns a list of all parents and their parents recursively - * - * @return \Kirby\Cms\Pages - */ - public function parents() - { - $parents = new Pages(); - $page = $this->parent(); - - while ($page !== null) { - $parents->append($page->id(), $page); - $page = $page->parent(); - } - - return $parents; - } - - /** - * Returns the permissions object for this page - * - * @return \Kirby\Cms\PagePermissions - */ - public function permissions() - { - return new PagePermissions($this); - } - - /** - * Draft preview Url - * - * @internal - * @return string|null - */ - public function previewUrl(): ?string - { - $preview = $this->blueprint()->preview(); - - if ($preview === false) { - return null; - } - - if ($preview === true) { - $url = $this->url(); - } else { - $url = $preview; - } - - if ($this->isDraft() === true) { - $uri = new Uri($url); - $uri->query->token = $this->token(); - - $url = $uri->toString(); - } - - return $url; - } - - /** - * Renders the page with the given data. - * - * An optional content type can be passed to - * render a content representation instead of - * the default template. - * - * @param array $data - * @param string $contentType - * @return string - * @throws \Kirby\Exception\NotFoundException If the default template cannot be found - */ - public function render(array $data = [], $contentType = 'html'): string - { - $kirby = $this->kirby(); - $cache = $cacheId = $html = null; - - // try to get the page from cache - if (empty($data) === true && $this->isCacheable() === true) { - $cache = $kirby->cache('pages'); - $cacheId = $this->cacheId($contentType); - $result = $cache->get($cacheId); - $html = $result['html'] ?? null; - $response = $result['response'] ?? []; - - // reconstruct the response configuration - if (empty($html) === false && empty($response) === false) { - $kirby->response()->fromArray($response); - } - } - - // fetch the page regularly - if ($html === null) { - if ($contentType === 'html') { - $template = $this->template(); - } else { - $template = $this->representation($contentType); - } - - if ($template->exists() === false) { - throw new NotFoundException([ - 'key' => 'template.default.notFound' - ]); - } - - $kirby->data = $this->controller($data, $contentType); - - // render the page - $html = $template->render($kirby->data); - - // convert the response configuration to an array - $response = $kirby->response()->toArray(); - - // cache the result - if ($cache !== null && $kirby->response()->cache() === true) { - $cache->set($cacheId, [ - 'html' => $html, - 'response' => $response - ], $kirby->response()->expires() ?? 0); - } - } - - return $html; - } - - /** - * @internal - * @param mixed $type - * @return \Kirby\Cms\Template - * @throws \Kirby\Exception\NotFoundException If the content representation cannot be found - */ - public function representation($type) - { - $kirby = $this->kirby(); - $template = $this->template(); - $representation = $kirby->template($template->name(), $type); - - if ($representation->exists() === true) { - return $representation; - } - - throw new NotFoundException('The content representation cannot be found'); - } - - /** - * Returns the absolute root to the page directory - * No matter if it exists or not. - * - * @return string - */ - public function root(): string - { - return $this->root ??= $this->kirby()->root('content') . '/' . $this->diruri(); - } - - /** - * Returns the PageRules class instance - * which is being used in various methods - * to check for valid actions and input. - * - * @return \Kirby\Cms\PageRules - */ - protected function rules() - { - return new PageRules(); - } - - /** - * Search all pages within the current page - * - * @param string|null $query - * @param array $params - * @return \Kirby\Cms\Pages - */ - public function search(string $query = null, $params = []) - { - return $this->index()->search($query, $params); - } - - /** - * Sets the Blueprint object - * - * @param array|null $blueprint - * @return $this - */ - protected function setBlueprint(array $blueprint = null) - { - if ($blueprint !== null) { - $blueprint['model'] = $this; - $this->blueprint = new PageBlueprint($blueprint); - } - - return $this; - } - - /** - * Sets the dirname manually, which works - * more reliable in connection with the inventory - * than computing the dirname afterwards - * - * @param string|null $dirname - * @return $this - */ - protected function setDirname(string $dirname = null) - { - $this->dirname = $dirname; - return $this; - } - - /** - * Sets the draft flag - * - * @param bool $isDraft - * @return $this - */ - protected function setIsDraft(bool $isDraft = null) - { - $this->isDraft = $isDraft ?? false; - return $this; - } - - /** - * Sets the sorting number - * - * @param int|null $num - * @return $this - */ - protected function setNum(int $num = null) - { - $this->num = $num === null ? $num : (int)$num; - return $this; - } - - /** - * Sets the parent page object - * - * @param \Kirby\Cms\Page|null $parent - * @return $this - */ - protected function setParent(Page $parent = null) - { - $this->parent = $parent; - return $this; - } - - /** - * Sets the absolute path to the page - * - * @param string|null $root - * @return $this - */ - protected function setRoot(string $root = null) - { - $this->root = $root; - return $this; - } - - /** - * Sets the required Page slug - * - * @param string $slug - * @return $this - */ - protected function setSlug(string $slug) - { - $this->slug = $slug; - return $this; - } - - /** - * Sets the intended template - * - * @param string|null $template - * @return $this - */ - protected function setTemplate(string $template = null) - { - if ($template !== null) { - $this->intendedTemplate = $this->kirby()->template($template); - } - - return $this; - } - - /** - * Sets the Url - * - * @param string|null $url - * @return $this - */ - protected function setUrl(string $url = null) - { - if (is_string($url) === true) { - $url = rtrim($url, '/'); - } - - $this->url = $url; - return $this; - } - - /** - * Returns the slug of the page - * - * @param string|null $languageCode - * @return string - */ - public function slug(string $languageCode = null): string - { - if ($this->kirby()->multilang() === true) { - if ($languageCode === null) { - $languageCode = $this->kirby()->languageCode(); - } - - $defaultLanguageCode = $this->kirby()->defaultLanguage()->code(); - - if ($languageCode !== $defaultLanguageCode && $translation = $this->translations()->find($languageCode)) { - return $translation->slug() ?? $this->slug; - } - } - - return $this->slug; - } - - /** - * Returns the page status, which - * can be `draft`, `listed` or `unlisted` - * - * @return string - */ - public function status(): string - { - if ($this->isDraft() === true) { - return 'draft'; - } - - if ($this->isUnlisted() === true) { - return 'unlisted'; - } - - return 'listed'; - } - - /** - * Returns the final template - * - * @return \Kirby\Cms\Template - */ - public function template() - { - if ($this->template !== null) { - return $this->template; - } - - $intended = $this->intendedTemplate(); - - if ($intended->exists() === true) { - return $this->template = $intended; - } - - return $this->template = $this->kirby()->template('default'); - } - - /** - * Returns the title field or the slug as fallback - * - * @return \Kirby\Cms\Field - */ - public function title() - { - return $this->content()->get('title')->or($this->slug()); - } - - /** - * Converts the most important - * properties to array - * - * @return array - */ - public function toArray(): array - { - return [ - 'children' => $this->children()->keys(), - 'content' => $this->content()->toArray(), - 'files' => $this->files()->keys(), - 'id' => $this->id(), - 'mediaUrl' => $this->mediaUrl(), - 'mediaRoot' => $this->mediaRoot(), - 'num' => $this->num(), - 'parent' => $this->parent() ? $this->parent()->id() : null, - 'slug' => $this->slug(), - 'template' => $this->template(), - 'translations' => $this->translations()->toArray(), - 'uid' => $this->uid(), - 'uri' => $this->uri(), - 'url' => $this->url() - ]; - } - - /** - * Returns a verification token, which - * is used for the draft authentication - * - * @return string - */ - protected function token(): string - { - return $this->kirby()->contentToken($this, $this->id() . $this->template()); - } - - /** - * Returns the UID of the page. - * The UID is basically the same as the - * slug, but stays the same on - * multi-language sites. Whereas the slug - * can be translated. - * - * @see self::slug() - * @return string - */ - public function uid(): string - { - return $this->slug; - } - - /** - * The uri is the same as the id, except - * that it will be translated in multi-language setups - * - * @param string|null $languageCode - * @return string - */ - public function uri(string $languageCode = null): string - { - // set the id, depending on the parent - if ($parent = $this->parent()) { - return $parent->uri($languageCode) . '/' . $this->slug($languageCode); - } - - return $this->slug($languageCode); - } - - /** - * Returns the Url - * - * @param array|string|null $options - * @return string - */ - public function url($options = null): string - { - if ($this->kirby()->multilang() === true) { - if (is_string($options) === true) { - return $this->urlForLanguage($options); - } else { - return $this->urlForLanguage(null, $options); - } - } - - if ($options !== null) { - return Url::to($this->url(), $options); - } - - if (is_string($this->url) === true) { - return $this->url; - } - - if ($this->isHomePage() === true) { - return $this->url = $this->site()->url(); - } - - if ($parent = $this->parent()) { - if ($parent->isHomePage() === true) { - return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid(); - } else { - return $this->url = $this->parent()->url() . '/' . $this->uid(); - } - } - - return $this->url = $this->kirby()->url('base') . '/' . $this->uid(); - } - - /** - * Builds the Url for a specific language - * - * @internal - * @param string|null $language - * @param array|null $options - * @return string - */ - public function urlForLanguage($language = null, array $options = null): string - { - if ($options !== null) { - return Url::to($this->urlForLanguage($language), $options); - } - - if ($this->isHomePage() === true) { - return $this->url = $this->site()->urlForLanguage($language); - } - - if ($parent = $this->parent()) { - if ($parent->isHomePage() === true) { - return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language); - } else { - return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language); - } - } - - return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language); - } - - - /** - * Deprecated! - */ - - /** - * Provides a kirbytag or markdown - * tag for the page, which will be - * used in the panel, when the page - * gets dragged onto a textarea - * - * @deprecated 3.6.0 Use `->panel()->dragText()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param string|null $type (null|auto|kirbytext|markdown) - * @return string - * @codeCoverageIgnore - */ - public function dragText(string $type = null): string - { - return $this->panel()->dragText($type); - } - - /** - * Returns the escaped Id, which is - * used in the panel to make routing work properly - * - * @deprecated 3.6.0 Use `->panel()->id()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @return string - * @codeCoverageIgnore - */ - public function panelId(): string - { - return $this->panel()->id(); - } - - /** - * Returns the full path without leading slash - * - * @deprecated 3.6.0 Use `->panel()->path()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @return string - * @codeCoverageIgnore - */ - public function panelPath(): string - { - return $this->panel()->path(); - } - - /** - * Prepares the response data for page pickers - * and page fields - * - * @deprecated 3.6.0 Use `->panel()->pickerData()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @param array|null $params - * @return array - * @codeCoverageIgnore - */ - public function panelPickerData(array $params = []): array - { - return $this->panel()->pickerData($params); - } - - /** - * Returns the url to the editing view - * in the panel - * - * @deprecated 3.6.0 Use `->panel()->url()` instead - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param bool $relative - * @return string - * @codeCoverageIgnore - */ - public function panelUrl(bool $relative = false): string - { - return $this->panel()->url($relative); - } + use PageActions; + use PageSiblings; + use HasChildren; + use HasFiles; + use HasMethods; + use HasSiblings; + + public const CLASS_ALIAS = 'page'; + + /** + * All registered page methods + * + * @var array + */ + public static $methods = []; + + /** + * Registry with all Page models + * + * @var array + */ + public static $models = []; + + /** + * The PageBlueprint object + * + * @var \Kirby\Cms\PageBlueprint + */ + protected $blueprint; + + /** + * Nesting level + * + * @var int + */ + protected $depth; + + /** + * Sorting number + slug + * + * @var string + */ + protected $dirname; + + /** + * Path of dirnames + * + * @var string + */ + protected $diruri; + + /** + * Draft status flag + * + * @var bool + */ + protected $isDraft; + + /** + * The Page id + * + * @var string + */ + protected $id; + + /** + * The template, that should be loaded + * if it exists + * + * @var \Kirby\Cms\Template + */ + protected $intendedTemplate; + + /** + * @var array + */ + protected $inventory; + + /** + * The sorting number + * + * @var int|null + */ + protected $num; + + /** + * The parent page + * + * @var \Kirby\Cms\Page|null + */ + protected $parent; + + /** + * Absolute path to the page directory + * + * @var string + */ + protected $root; + + /** + * The parent Site object + * + * @var \Kirby\Cms\Site|null + */ + protected $site; + + /** + * The URL-appendix aka slug + * + * @var string + */ + protected $slug; + + /** + * The intended page template + * + * @var \Kirby\Cms\Template + */ + protected $template; + + /** + * The page url + * + * @var string|null + */ + protected $url; + + /** + * Magic caller + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // page methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // return page content otherwise + return $this->content()->get($method); + } + + /** + * Creates a new page object + * + * @param array $props + */ + public function __construct(array $props) + { + // set the slug as the first property + $this->slug = $props['slug'] ?? null; + + // add all other properties + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'content' => $this->content(), + 'children' => $this->children(), + 'siblings' => $this->siblings(), + 'translations' => $this->translations(), + 'files' => $this->files(), + ]); + } + + /** + * Returns the url to the api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + if ($relative === true) { + return 'pages/' . $this->panel()->id(); + } else { + return $this->kirby()->url('api') . '/pages/' . $this->panel()->id(); + } + } + + /** + * Returns the blueprint object + * + * @return \Kirby\Cms\PageBlueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\PageBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = PageBlueprint::factory('pages/' . $this->intendedTemplate(), 'pages/default', $this); + } + + /** + * Returns an array with all blueprints that are available for the page + * + * @param string|null $inSection + * @return array + */ + public function blueprints(?string $inSection = null): array + { + if ($inSection !== null) { + return $this->blueprint()->section($inSection)->blueprints(); + } + + $blueprints = []; + $templates = $this->blueprint()->changeTemplate() ?? $this->blueprint()->options()['changeTemplate'] ?? []; + $currentTemplate = $this->intendedTemplate()->name(); + + if (is_array($templates) === false) { + $templates = []; + } + + // add the current template to the array if it's not already there + if (in_array($currentTemplate, $templates) === false) { + array_unshift($templates, $currentTemplate); + } + + // make sure every template is only included once + $templates = array_unique($templates); + + foreach ($templates as $template) { + try { + $props = Blueprint::load('pages/' . $template); + + $blueprints[] = [ + 'name' => basename($props['name']), + 'title' => $props['title'], + ]; + } catch (Exception $e) { + // skip invalid blueprints + } + } + + return array_values($blueprints); + } + + /** + * Builds the cache id for the page + * + * @param string $contentType + * @return string + */ + protected function cacheId(string $contentType): string + { + $cacheId = [$this->id()]; + + if ($this->kirby()->multilang() === true) { + $cacheId[] = $this->kirby()->language()->code(); + } + + $cacheId[] = $contentType; + + return implode('.', $cacheId); + } + + /** + * Prepares the content for the write method + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, ?string $languageCode = null): array + { + return A::prepend($data, [ + 'title' => $data['title'] ?? null, + 'slug' => $data['slug'] ?? null + ]); + } + + /** + * Returns the content text file + * which is found by the inventory method + * + * @internal + * @param string|null $languageCode + * @return string + */ + public function contentFileName(?string $languageCode = null): string + { + return $this->intendedTemplate()->name(); + } + + /** + * Call the page controller + * + * @internal + * @param array $data + * @param string $contentType + * @return array + * @throws \Kirby\Exception\InvalidArgumentException If the controller returns invalid objects for `kirby`, `site`, `pages` or `page` + */ + public function controller(array $data = [], string $contentType = 'html'): array + { + // create the template data + $data = array_merge($data, [ + 'kirby' => $kirby = $this->kirby(), + 'site' => $site = $this->site(), + 'pages' => $site->children(), + 'page' => $site->visit($this) + ]); + + // call the template controller if there's one. + $controllerData = $kirby->controller($this->template()->name(), $data, $contentType); + + // merge controller data with original data safely + if (empty($controllerData) === false) { + $classes = [ + 'kirby' => 'Kirby\Cms\App', + 'site' => 'Kirby\Cms\Site', + 'pages' => 'Kirby\Cms\Pages', + 'page' => 'Kirby\Cms\Page' + ]; + + foreach ($controllerData as $key => $value) { + if (array_key_exists($key, $classes) === true) { + if (is_a($value, $classes[$key]) === true) { + $data[$key] = $value; + } else { + throw new InvalidArgumentException('The returned variable "' . $key . '" from the controller "' . $this->template()->name() . '" is not of the required type "' . $classes[$key] . '"'); + } + } else { + $data[$key] = $value; + } + } + } + + return $data; + } + + /** + * Returns a number indicating how deep the page + * is nested within the content folder + * + * @return int + */ + public function depth(): int + { + return $this->depth ??= (substr_count($this->id(), '/') + 1); + } + + /** + * Sorting number + Slug + * + * @return string + */ + public function dirname(): string + { + if ($this->dirname !== null) { + return $this->dirname; + } + + if ($this->num() !== null) { + return $this->dirname = $this->num() . Dir::$numSeparator . $this->uid(); + } else { + return $this->dirname = $this->uid(); + } + } + + /** + * Sorting number + Slug + * + * @return string + */ + public function diruri(): string + { + if (is_string($this->diruri) === true) { + return $this->diruri; + } + + if ($this->isDraft() === true) { + $dirname = '_drafts/' . $this->dirname(); + } else { + $dirname = $this->dirname(); + } + + if ($parent = $this->parent()) { + return $this->diruri = $parent->diruri() . '/' . $dirname; + } else { + return $this->diruri = $dirname; + } + } + + /** + * Checks if the page exists on disk + * + * @return bool + */ + public function exists(): bool + { + return is_dir($this->root()) === true; + } + + /** + * Constructs a Page object and also + * takes page models into account. + * + * @internal + * @param mixed $props + * @return static + */ + public static function factory($props) + { + if (empty($props['model']) === false) { + return static::model($props['model'], $props); + } + + return new static($props); + } + + /** + * Redirects to this page, + * wrapper for the `go()` helper + * + * @since 3.4.0 + * + * @param array $options Options for `Kirby\Http\Uri` to create URL parts + * @param int $code HTTP status code + */ + public function go(array $options = [], int $code = 302) + { + Response::go($this->url($options), $code); + } + + /** + * Checks if the intended template + * for the page exists. + * + * @return bool + */ + public function hasTemplate(): bool + { + return $this->intendedTemplate() === $this->template(); + } + + /** + * Returns the Page Id + * + * @return string + */ + public function id(): string + { + if ($this->id !== null) { + return $this->id; + } + + // set the id, depending on the parent + if ($parent = $this->parent()) { + return $this->id = $parent->id() . '/' . $this->uid(); + } + + return $this->id = $this->uid(); + } + + /** + * Returns the template that should be + * loaded if it exists. + * + * @return \Kirby\Cms\Template + */ + public function intendedTemplate() + { + if ($this->intendedTemplate !== null) { + return $this->intendedTemplate; + } + + return $this->setTemplate($this->inventory()['template'])->intendedTemplate(); + } + + /** + * Returns the inventory of files + * children and content files + * + * @internal + * @return array + */ + public function inventory(): array + { + if ($this->inventory !== null) { + return $this->inventory; + } + + $kirby = $this->kirby(); + + return $this->inventory = Dir::inventory( + $this->root(), + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + } + + /** + * Compares the current object with the given page object + * + * @param \Kirby\Cms\Page|string $page + * @return bool + */ + public function is($page): bool + { + if (is_a($page, 'Kirby\Cms\Page') === false) { + if (is_string($page) === false) { + return false; + } + + $page = $this->kirby()->page($page); + } + + if (is_a($page, 'Kirby\Cms\Page') === false) { + return false; + } + + return $this->id() === $page->id(); + } + + /** + * Checks if the page is the current page + * + * @return bool + */ + public function isActive(): bool + { + if ($page = $this->site()->page()) { + if ($page->is($this) === true) { + return true; + } + } + + return false; + } + + /** + * Checks if the page is a direct or indirect ancestor of the given $page object + * + * @param Page $child + * @return bool + */ + public function isAncestorOf(Page $child): bool + { + return $child->parents()->has($this->id()) === true; + } + + /** + * Checks if the page can be cached in the + * pages cache. This will also check if one + * of the ignore rules from the config kick in. + * + * @return bool + */ + public function isCacheable(): bool + { + $kirby = $this->kirby(); + $cache = $kirby->cache('pages'); + $options = $cache->options(); + $ignore = $options['ignore'] ?? null; + + // the pages cache is switched off + if (($options['active'] ?? false) === false) { + return false; + } + + // inspect the current request + $request = $kirby->request(); + + // disable the pages cache for any request types but GET or HEAD + if (in_array($request->method(), ['GET', 'HEAD']) === false) { + return false; + } + + // disable the pages cache when there's request data + if (empty($request->data()) === false) { + return false; + } + + // disable the pages cache when there are any params + if ($request->params()->isNotEmpty()) { + return false; + } + + // check for a custom ignore rule + if (is_a($ignore, 'Closure') === true) { + if ($ignore($this) === true) { + return false; + } + } + + // ignore pages by id + if (is_array($ignore) === true) { + if (in_array($this->id(), $ignore) === true) { + return false; + } + } + + return true; + } + + /** + * Checks if the page is a child of the given page + * + * @param \Kirby\Cms\Page|string $parent + * @return bool + */ + public function isChildOf($parent): bool + { + if ($parentObj = $this->parent()) { + return $parentObj->is($parent); + } + + return false; + } + + /** + * Checks if the page is a descendant of the given page + * + * @param \Kirby\Cms\Page|string $parent + * @return bool + */ + public function isDescendantOf($parent): bool + { + if (is_string($parent) === true) { + $parent = $this->site()->find($parent); + } + + if (!$parent) { + return false; + } + + return $this->parents()->has($parent->id()) === true; + } + + /** + * Checks if the page is a descendant of the currently active page + * + * @return bool + */ + public function isDescendantOfActive(): bool + { + if ($active = $this->site()->page()) { + return $this->isDescendantOf($active); + } + + return false; + } + + /** + * Checks if the current page is a draft + * + * @return bool + */ + public function isDraft(): bool + { + return $this->isDraft; + } + + /** + * Checks if the page is the error page + * + * @return bool + */ + public function isErrorPage(): bool + { + return $this->id() === $this->site()->errorPageId(); + } + + /** + * Checks if the page is the home page + * + * @return bool + */ + public function isHomePage(): bool + { + return $this->id() === $this->site()->homePageId(); + } + + /** + * It's often required to check for the + * home and error page to stop certain + * actions. That's why there's a shortcut. + * + * @return bool + */ + public function isHomeOrErrorPage(): bool + { + return $this->isHomePage() === true || $this->isErrorPage() === true; + } + + /** + * Checks if the page has a sorting number + * + * @return bool + */ + public function isListed(): bool + { + return $this->num() !== null; + } + + /** + * Checks if the page is open. + * Open pages are either the current one + * or descendants of the current one. + * + * @return bool + */ + public function isOpen(): bool + { + if ($this->isActive() === true) { + return true; + } + + if ($page = $this->site()->page()) { + if ($page->parents()->has($this->id()) === true) { + return true; + } + } + + return false; + } + + /** + * Checks if the page is not a draft. + * + * @return bool + */ + public function isPublished(): bool + { + return $this->isDraft() === false; + } + + /** + * Check if the page can be read by the current user + * + * @return bool + */ + public function isReadable(): bool + { + static $readable = []; + + $template = $this->intendedTemplate()->name(); + + if (isset($readable[$template]) === true) { + return $readable[$template]; + } + + return $readable[$template] = $this->permissions()->can('read'); + } + + /** + * Checks if the page is sortable + * + * @return bool + */ + public function isSortable(): bool + { + return $this->permissions()->can('sort'); + } + + /** + * Checks if the page has no sorting number + * + * @return bool + */ + public function isUnlisted(): bool + { + return $this->isListed() === false; + } + + /** + * Checks if the page access is verified. + * This is only used for drafts so far. + * + * @internal + * @param string|null $token + * @return bool + */ + public function isVerified(string $token = null) + { + if ( + $this->isDraft() === false && + $this->parents()->findBy('status', 'draft') === null + ) { + return true; + } + + if ($token === null) { + return false; + } + + return $this->token() === $token; + } + + /** + * Returns the root to the media folder for the page + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/pages/' . $this->id(); + } + + /** + * The page's base URL for any files + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/pages/' . $this->id(); + } + + /** + * Creates a page model if it has been registered + * + * @internal + * @param string $name + * @param array $props + * @return static + */ + public static function model(string $name, array $props = []) + { + if ($class = (static::$models[$name] ?? null)) { + $object = new $class($props); + + if (is_a($object, 'Kirby\Cms\Page') === true) { + return $object; + } + } + + return new static($props); + } + + /** + * Returns the last modification date of the page + * + * @param string|null $format + * @param string|null $handler + * @param string|null $languageCode + * @return int|string + */ + public function modified(string $format = null, string $handler = null, string $languageCode = null) + { + return F::modified( + $this->contentFile($languageCode), + $format, + $handler ?? $this->kirby()->option('date.handler', 'date') + ); + } + + /** + * Returns the sorting number + * + * @return int|null + */ + public function num(): ?int + { + return $this->num; + } + + /** + * Returns the panel info object + * + * @return \Kirby\Panel\Page + */ + public function panel() + { + return new Panel($this); + } + + /** + * Returns the parent Page object + * + * @return \Kirby\Cms\Page|null + */ + public function parent() + { + return $this->parent; + } + + /** + * Returns the parent id, if a parent exists + * + * @internal + * @return string|null + */ + public function parentId(): ?string + { + if ($parent = $this->parent()) { + return $parent->id(); + } + + return null; + } + + /** + * Returns the parent model, + * which can either be another Page + * or the Site + * + * @internal + * @return \Kirby\Cms\Page|\Kirby\Cms\Site + */ + public function parentModel() + { + return $this->parent() ?? $this->site(); + } + + /** + * Returns a list of all parents and their parents recursively + * + * @return \Kirby\Cms\Pages + */ + public function parents() + { + $parents = new Pages(); + $page = $this->parent(); + + while ($page !== null) { + $parents->append($page->id(), $page); + $page = $page->parent(); + } + + return $parents; + } + + /** + * Returns the permissions object for this page + * + * @return \Kirby\Cms\PagePermissions + */ + public function permissions() + { + return new PagePermissions($this); + } + + /** + * Draft preview Url + * + * @internal + * @return string|null + */ + public function previewUrl(): ?string + { + $preview = $this->blueprint()->preview(); + + if ($preview === false) { + return null; + } + + if ($preview === true) { + $url = $this->url(); + } else { + $url = $preview; + } + + if ($this->isDraft() === true) { + $uri = new Uri($url); + $uri->query->token = $this->token(); + + $url = $uri->toString(); + } + + return $url; + } + + /** + * Renders the page with the given data. + * + * An optional content type can be passed to + * render a content representation instead of + * the default template. + * + * @param array $data + * @param string $contentType + * @return string + * @throws \Kirby\Exception\NotFoundException If the default template cannot be found + */ + public function render(array $data = [], $contentType = 'html'): string + { + $kirby = $this->kirby(); + $cache = $cacheId = $html = null; + + // try to get the page from cache + if (empty($data) === true && $this->isCacheable() === true) { + $cache = $kirby->cache('pages'); + $cacheId = $this->cacheId($contentType); + $result = $cache->get($cacheId); + $html = $result['html'] ?? null; + $response = $result['response'] ?? []; + $usesAuth = $result['usesAuth'] ?? false; + $usesCookies = $result['usesCookies'] ?? []; + + // if the request contains dynamic data that the cached response + // relied on, don't use the cache to allow dynamic code to run + if (Responder::isPrivate($usesAuth, $usesCookies) === true) { + $html = null; + } + + // reconstruct the response configuration + if (empty($html) === false && empty($response) === false) { + $kirby->response()->fromArray($response); + } + } + + // fetch the page regularly + if ($html === null) { + if ($contentType === 'html') { + $template = $this->template(); + } else { + $template = $this->representation($contentType); + } + + if ($template->exists() === false) { + throw new NotFoundException([ + 'key' => 'template.default.notFound' + ]); + } + + $kirby->data = $this->controller($data, $contentType); + + // render the page + $html = $template->render($kirby->data); + + // cache the result + $response = $kirby->response(); + if ($cache !== null && $response->cache() === true) { + $cache->set($cacheId, [ + 'html' => $html, + 'response' => $response->toArray(), + 'usesAuth' => $response->usesAuth(), + 'usesCookies' => $response->usesCookies(), + ], $response->expires() ?? 0); + } + } + + return $html; + } + + /** + * @internal + * @param mixed $type + * @return \Kirby\Cms\Template + * @throws \Kirby\Exception\NotFoundException If the content representation cannot be found + */ + public function representation($type) + { + $kirby = $this->kirby(); + $template = $this->template(); + $representation = $kirby->template($template->name(), $type); + + if ($representation->exists() === true) { + return $representation; + } + + throw new NotFoundException('The content representation cannot be found'); + } + + /** + * Returns the absolute root to the page directory + * No matter if it exists or not. + * + * @return string + */ + public function root(): string + { + return $this->root ??= $this->kirby()->root('content') . '/' . $this->diruri(); + } + + /** + * Returns the PageRules class instance + * which is being used in various methods + * to check for valid actions and input. + * + * @return \Kirby\Cms\PageRules + */ + protected function rules() + { + return new PageRules(); + } + + /** + * Search all pages within the current page + * + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Pages + */ + public function search(string $query = null, $params = []) + { + return $this->index()->search($query, $params); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return $this + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new PageBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the dirname manually, which works + * more reliable in connection with the inventory + * than computing the dirname afterwards + * + * @param string|null $dirname + * @return $this + */ + protected function setDirname(string $dirname = null) + { + $this->dirname = $dirname; + return $this; + } + + /** + * Sets the draft flag + * + * @param bool $isDraft + * @return $this + */ + protected function setIsDraft(bool $isDraft = null) + { + $this->isDraft = $isDraft ?? false; + return $this; + } + + /** + * Sets the sorting number + * + * @param int|null $num + * @return $this + */ + protected function setNum(int $num = null) + { + $this->num = $num === null ? $num : (int)$num; + return $this; + } + + /** + * Sets the parent page object + * + * @param \Kirby\Cms\Page|null $parent + * @return $this + */ + protected function setParent(Page $parent = null) + { + $this->parent = $parent; + return $this; + } + + /** + * Sets the absolute path to the page + * + * @param string|null $root + * @return $this + */ + protected function setRoot(string $root = null) + { + $this->root = $root; + return $this; + } + + /** + * Sets the required Page slug + * + * @param string $slug + * @return $this + */ + protected function setSlug(string $slug) + { + $this->slug = $slug; + return $this; + } + + /** + * Sets the intended template + * + * @param string|null $template + * @return $this + */ + protected function setTemplate(string $template = null) + { + if ($template !== null) { + $this->intendedTemplate = $this->kirby()->template($template); + } + + return $this; + } + + /** + * Sets the Url + * + * @param string|null $url + * @return $this + */ + protected function setUrl(string $url = null) + { + if (is_string($url) === true) { + $url = rtrim($url, '/'); + } + + $this->url = $url; + return $this; + } + + /** + * Returns the slug of the page + * + * @param string|null $languageCode + * @return string + */ + public function slug(string $languageCode = null): string + { + if ($this->kirby()->multilang() === true) { + if ($languageCode === null) { + $languageCode = $this->kirby()->languageCode(); + } + + $defaultLanguageCode = $this->kirby()->defaultLanguage()->code(); + + if ($languageCode !== $defaultLanguageCode && $translation = $this->translations()->find($languageCode)) { + return $translation->slug() ?? $this->slug; + } + } + + return $this->slug; + } + + /** + * Returns the page status, which + * can be `draft`, `listed` or `unlisted` + * + * @return string + */ + public function status(): string + { + if ($this->isDraft() === true) { + return 'draft'; + } + + if ($this->isUnlisted() === true) { + return 'unlisted'; + } + + return 'listed'; + } + + /** + * Returns the final template + * + * @return \Kirby\Cms\Template + */ + public function template() + { + if ($this->template !== null) { + return $this->template; + } + + $intended = $this->intendedTemplate(); + + if ($intended->exists() === true) { + return $this->template = $intended; + } + + return $this->template = $this->kirby()->template('default'); + } + + /** + * Returns the title field or the slug as fallback + * + * @return \Kirby\Cms\Field + */ + public function title() + { + return $this->content()->get('title')->or($this->slug()); + } + + /** + * Converts the most important + * properties to array + * + * @return array + */ + public function toArray(): array + { + return [ + 'children' => $this->children()->keys(), + 'content' => $this->content()->toArray(), + 'files' => $this->files()->keys(), + 'id' => $this->id(), + 'mediaUrl' => $this->mediaUrl(), + 'mediaRoot' => $this->mediaRoot(), + 'num' => $this->num(), + 'parent' => $this->parent() ? $this->parent()->id() : null, + 'slug' => $this->slug(), + 'template' => $this->template(), + 'translations' => $this->translations()->toArray(), + 'uid' => $this->uid(), + 'uri' => $this->uri(), + 'url' => $this->url() + ]; + } + + /** + * Returns a verification token, which + * is used for the draft authentication + * + * @return string + */ + protected function token(): string + { + return $this->kirby()->contentToken($this, $this->id() . $this->template()); + } + + /** + * Returns the UID of the page. + * The UID is basically the same as the + * slug, but stays the same on + * multi-language sites. Whereas the slug + * can be translated. + * + * @see self::slug() + * @return string + */ + public function uid(): string + { + return $this->slug; + } + + /** + * The uri is the same as the id, except + * that it will be translated in multi-language setups + * + * @param string|null $languageCode + * @return string + */ + public function uri(string $languageCode = null): string + { + // set the id, depending on the parent + if ($parent = $this->parent()) { + return $parent->uri($languageCode) . '/' . $this->slug($languageCode); + } + + return $this->slug($languageCode); + } + + /** + * Returns the Url + * + * @param array|string|null $options + * @return string + */ + public function url($options = null): string + { + if ($this->kirby()->multilang() === true) { + if (is_string($options) === true) { + return $this->urlForLanguage($options); + } else { + return $this->urlForLanguage(null, $options); + } + } + + if ($options !== null) { + return Url::to($this->url(), $options); + } + + if (is_string($this->url) === true) { + return $this->url; + } + + if ($this->isHomePage() === true) { + return $this->url = $this->site()->url(); + } + + if ($parent = $this->parent()) { + if ($parent->isHomePage() === true) { + return $this->url = $this->kirby()->url('base') . '/' . $parent->uid() . '/' . $this->uid(); + } else { + return $this->url = $this->parent()->url() . '/' . $this->uid(); + } + } + + return $this->url = $this->kirby()->url('base') . '/' . $this->uid(); + } + + /** + * Builds the Url for a specific language + * + * @internal + * @param string|null $language + * @param array|null $options + * @return string + */ + public function urlForLanguage($language = null, array $options = null): string + { + if ($options !== null) { + return Url::to($this->urlForLanguage($language), $options); + } + + if ($this->isHomePage() === true) { + return $this->url = $this->site()->urlForLanguage($language); + } + + if ($parent = $this->parent()) { + if ($parent->isHomePage() === true) { + return $this->url = $this->site()->urlForLanguage($language) . '/' . $parent->slug($language) . '/' . $this->slug($language); + } else { + return $this->url = $this->parent()->urlForLanguage($language) . '/' . $this->slug($language); + } + } + + return $this->url = $this->site()->urlForLanguage($language) . '/' . $this->slug($language); + } + + + /** + * Deprecated! + */ + + /** + * Provides a kirbytag or markdown + * tag for the page, which will be + * used in the panel, when the page + * gets dragged onto a textarea + * + * @deprecated 3.6.0 Use `->panel()->dragText()` instead + * @todo Remove in 3.8.0 + * + * @internal + * @param string|null $type (null|auto|kirbytext|markdown) + * @return string + * @codeCoverageIgnore + */ + public function dragText(string $type = null): string + { + Helpers::deprecated('Cms\Page::dragText() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->dragText() instead.'); + return $this->panel()->dragText($type); + } + + /** + * Returns the escaped Id, which is + * used in the panel to make routing work properly + * + * @deprecated 3.6.0 Use `->panel()->id()` instead + * @todo Remove in 3.8.0 + * + * @internal + * @return string + * @codeCoverageIgnore + */ + public function panelId(): string + { + Helpers::deprecated('Cms\Page::panelId() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->id() instead.'); + return $this->panel()->id(); + } + + /** + * Returns the full path without leading slash + * + * @deprecated 3.6.0 Use `->panel()->path()` instead + * @todo Remove in 3.8.0 + * + * @internal + * @return string + * @codeCoverageIgnore + */ + public function panelPath(): string + { + Helpers::deprecated('Cms\Page::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->path() instead.'); + return $this->panel()->path(); + } + + /** + * Prepares the response data for page pickers + * and page fields + * + * @deprecated 3.6.0 Use `->panel()->pickerData()` instead + * @todo Remove in 3.8.0 + * + * @param array|null $params + * @return array + * @codeCoverageIgnore + */ + public function panelPickerData(array $params = []): array + { + Helpers::deprecated('Cms\Page::panelPickerData() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->pickerData() instead.'); + return $this->panel()->pickerData($params); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @deprecated 3.6.0 Use `->panel()->url()` instead + * @todo Remove in 3.8.0 + * + * @internal + * @param bool $relative + * @return string + * @codeCoverageIgnore + */ + public function panelUrl(bool $relative = false): string + { + Helpers::deprecated('Cms\Page::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $page->panel()->url() instead.'); + return $this->panel()->url($relative); + } } diff --git a/kirby/src/Cms/PageActions.php b/kirby/src/Cms/PageActions.php index 364ed5b..ec9115c 100644 --- a/kirby/src/Cms/PageActions.php +++ b/kirby/src/Cms/PageActions.php @@ -11,6 +11,7 @@ use Kirby\Filesystem\Dir; use Kirby\Filesystem\F; use Kirby\Form\Form; use Kirby\Toolkit\A; +use Kirby\Toolkit\I18n; use Kirby\Toolkit\Str; /** @@ -24,855 +25,878 @@ use Kirby\Toolkit\Str; */ trait PageActions { - /** - * Changes the sorting number. - * The sorting number must already be correct - * when the method is called. - * This only affects this page, - * siblings will not be resorted. - * - * @param int|null $num - * @return $this|static - * @throws \Kirby\Exception\LogicException If a draft is being sorted or the directory cannot be moved - */ - public function changeNum(int $num = null) - { - if ($this->isDraft() === true) { - throw new LogicException('Drafts cannot change their sorting number'); - } - - // don't run the action if everything stayed the same - if ($this->num() === $num) { - return $this; - } - - return $this->commit('changeNum', ['page' => $this, 'num' => $num], function ($oldPage, $num) { - $newPage = $oldPage->clone([ - 'num' => $num, - 'dirname' => null, - 'root' => null - ]); - - // actually move the page on disk - if ($oldPage->exists() === true) { - if (Dir::move($oldPage->root(), $newPage->root()) === true) { - // Updates the root path of the old page with the root path - // of the moved new page to use fly actions on old page in loop - $oldPage->setRoot($newPage->root()); - } else { - throw new LogicException('The page directory cannot be moved'); - } - } - - // overwrite the child in the parent page - $newPage - ->parentModel() - ->children() - ->set($newPage->id(), $newPage); - - return $newPage; - }); - } - - /** - * Changes the slug/uid of the page - * - * @param string $slug - * @param string|null $languageCode - * @return $this|static - * @throws \Kirby\Exception\LogicException If the directory cannot be moved - */ - public function changeSlug(string $slug, string $languageCode = null) - { - // always sanitize the slug - $slug = Str::slug($slug); - - // in multi-language installations the slug for the non-default - // languages is stored in the text file. The changeSlugForLanguage - // method takes care of that. - if ($language = $this->kirby()->language($languageCode)) { - if ($language->isDefault() === false) { - return $this->changeSlugForLanguage($slug, $languageCode); - } - } - - // if the slug stays exactly the same, - // nothing needs to be done. - if ($slug === $this->slug()) { - return $this; - } - - $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => null]; - return $this->commit('changeSlug', $arguments, function ($oldPage, $slug) { - $newPage = $oldPage->clone([ - 'slug' => $slug, - 'dirname' => null, - 'root' => null - ]); - - if ($oldPage->exists() === true) { - // remove the lock of the old page - if ($lock = $oldPage->lock()) { - $lock->remove(); - } - - // actually move stuff on disk - if (Dir::move($oldPage->root(), $newPage->root()) !== true) { - throw new LogicException('The page directory cannot be moved'); - } - - // remove from the siblings - $oldPage->parentModel()->children()->remove($oldPage); - - Dir::remove($oldPage->mediaRoot()); - } - - // overwrite the new page in the parent collection - if ($newPage->isDraft() === true) { - $newPage->parentModel()->drafts()->set($newPage->id(), $newPage); - } else { - $newPage->parentModel()->children()->set($newPage->id(), $newPage); - } - - return $newPage; - }); - } - - /** - * Change the slug for a specific language - * - * @param string $slug - * @param string|null $languageCode - * @return static - * @throws \Kirby\Exception\NotFoundException If the language for the given language code cannot be found - * @throws \Kirby\Exception\InvalidArgumentException If the slug for the default language is being changed - */ - protected function changeSlugForLanguage(string $slug, string $languageCode = null) - { - $language = $this->kirby()->language($languageCode); - - if (!$language) { - throw new NotFoundException('The language: "' . $languageCode . '" does not exist'); - } - - if ($language->isDefault() === true) { - throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language'); - } - - $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode]; - return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) { - // remove the slug if it's the same as the folder name - if ($slug === $page->uid()) { - $slug = null; - } - - return $page->save(['slug' => $slug], $languageCode); - }); - } - - /** - * Change the status of the current page - * to either draft, listed or unlisted. - * If changing to `listed`, you can pass a position for the - * page in the siblings collection. Siblings will be resorted. - * - * @param string $status "draft", "listed" or "unlisted" - * @param int|null $position Optional sorting number - * @return static - * @throws \Kirby\Exception\InvalidArgumentException If an invalid status is being passed - */ - public function changeStatus(string $status, int $position = null) - { - switch ($status) { - case 'draft': - return $this->changeStatusToDraft(); - case 'listed': - return $this->changeStatusToListed($position); - case 'unlisted': - return $this->changeStatusToUnlisted(); - default: - throw new InvalidArgumentException('Invalid status: ' . $status); - } - } - - /** - * @return static - */ - protected function changeStatusToDraft() - { - $arguments = ['page' => $this, 'status' => 'draft', 'position' => null]; - $page = $this->commit( - 'changeStatus', - $arguments, - fn ($page) => $page->unpublish() - ); - - return $page; - } - - /** - * @param int|null $position - * @return $this|static - */ - protected function changeStatusToListed(int $position = null) - { - // create a sorting number for the page - $num = $this->createNum($position); - - // don't sort if not necessary - if ($this->status() === 'listed' && $num === $this->num()) { - return $this; - } - - $arguments = ['page' => $this, 'status' => 'listed', 'position' => $num]; - $page = $this->commit('changeStatus', $arguments, function ($page, $status, $position) { - return $page->publish()->changeNum($position); - }); - - if ($this->blueprint()->num() === 'default') { - $page->resortSiblingsAfterListing($num); - } - - return $page; - } - - /** - * @return $this|static - */ - protected function changeStatusToUnlisted() - { - if ($this->status() === 'unlisted') { - return $this; - } - - $arguments = ['page' => $this, 'status' => 'unlisted', 'position' => null]; - $page = $this->commit('changeStatus', $arguments, function ($page) { - return $page->publish()->changeNum(null); - }); - - $this->resortSiblingsAfterUnlisting(); - - return $page; - } - - /** - * Change the position of the page in its siblings - * collection. Siblings will be resorted. If the page - * status isn't yet `listed`, it will be changed to it. - * - * @param int|null $position - * @return $this|static - */ - public function changeSort(int $position = null) - { - return $this->changeStatus('listed', $position); - } - - /** - * Changes the page template - * - * @param string $template - * @return $this|static - * @throws \Kirby\Exception\LogicException If the textfile cannot be renamed/moved - */ - public function changeTemplate(string $template) - { - if ($template === $this->intendedTemplate()->name()) { - return $this; - } - - return $this->commit('changeTemplate', ['page' => $this, 'template' => $template], function ($oldPage, $template) { - if ($this->kirby()->multilang() === true) { - $newPage = $this->clone([ - 'template' => $template - ]); - - foreach ($this->kirby()->languages()->codes() as $code) { - if ($oldPage->translation($code)->exists() !== true) { - continue; - } - - $content = $oldPage->content($code)->convertTo($template); - - if (F::remove($oldPage->contentFile($code)) !== true) { - throw new LogicException('The old text file could not be removed'); - } - - // save the language file - $newPage->save($content, $code); - } - - // return a fresh copy of the object - $page = $newPage->clone(); - } else { - $newPage = $this->clone([ - 'content' => $this->content()->convertTo($template), - 'template' => $template - ]); - - if (F::remove($oldPage->contentFile()) !== true) { - throw new LogicException('The old text file could not be removed'); - } - - $page = $newPage->save(); - } - - // update the parent collection - if ($page->isDraft() === true) { - $page->parentModel()->drafts()->set($page->id(), $page); - } else { - $page->parentModel()->children()->set($page->id(), $page); - } - - return $page; - }); - } - - /** - * Change the page title - * - * @param string $title - * @param string|null $languageCode - * @return static - */ - public function changeTitle(string $title, string $languageCode = null) - { - $arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode]; - return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) { - $page = $page->save(['title' => $title], $languageCode); - - // flush the parent cache to get children and drafts right - if ($page->isDraft() === true) { - $page->parentModel()->drafts()->set($page->id(), $page); - } else { - $page->parentModel()->children()->set($page->id(), $page); - } - - return $page; - }); - } - - /** - * Commits a page action, by following these steps - * - * 1. checks the action rules - * 2. sends the before hook - * 3. commits the store action - * 4. sends the after hook - * 5. returns the result - * - * @param string $action - * @param array $arguments - * @param \Closure $callback - * @return mixed - */ - protected function commit(string $action, array $arguments, Closure $callback) - { - $old = $this->hardcopy(); - $kirby = $this->kirby(); - $argumentValues = array_values($arguments); - - $this->rules()->$action(...$argumentValues); - $kirby->trigger('page.' . $action . ':before', $arguments); - - $result = $callback(...$argumentValues); - - if ($action === 'create') { - $argumentsAfter = ['page' => $result]; - } elseif ($action === 'duplicate') { - $argumentsAfter = ['duplicatePage' => $result, 'originalPage' => $old]; - } elseif ($action === 'delete') { - $argumentsAfter = ['status' => $result, 'page' => $old]; - } else { - $argumentsAfter = ['newPage' => $result, 'oldPage' => $old]; - } - $kirby->trigger('page.' . $action . ':after', $argumentsAfter); - - $kirby->cache('pages')->flush(); - return $result; - } - - /** - * Copies the page to a new parent - * - * @param array $options - * @return \Kirby\Cms\Page - * @throws \Kirby\Exception\DuplicateException If the page already exists - */ - public function copy(array $options = []) - { - $slug = $options['slug'] ?? $this->slug(); - $isDraft = $options['isDraft'] ?? $this->isDraft(); - $parent = $options['parent'] ?? null; - $parentModel = $options['parent'] ?? $this->site(); - $num = $options['num'] ?? null; - $children = $options['children'] ?? false; - $files = $options['files'] ?? false; - - // clean up the slug - $slug = Str::slug($slug); - - if ($parentModel->findPageOrDraft($slug)) { - throw new DuplicateException([ - 'key' => 'page.duplicate', - 'data' => [ - 'slug' => $slug - ] - ]); - } - - $tmp = new static([ - 'isDraft' => $isDraft, - 'num' => $num, - 'parent' => $parent, - 'slug' => $slug, - ]); - - $ignore = [ - $this->kirby()->locks()->file($this) - ]; - - // don't copy files - if ($files === false) { - foreach ($this->files() as $file) { - $ignore[] = $file->root(); - - // append all content files - array_push($ignore, ...$file->contentFiles()); - } - } - - Dir::copy($this->root(), $tmp->root(), $children, $ignore); - - $copy = $parentModel->clone()->findPageOrDraft($slug); - - // remove all translated slugs - if ($this->kirby()->multilang() === true) { - foreach ($this->kirby()->languages() as $language) { - if ($language->isDefault() === false && $copy->translation($language)->exists() === true) { - $copy = $copy->save(['slug' => null], $language->code()); - } - } - } - - // add copy to siblings - if ($isDraft === true) { - $parentModel->drafts()->append($copy->id(), $copy); - } else { - $parentModel->children()->append($copy->id(), $copy); - } - - return $copy; - } - - /** - * Creates and stores a new page - * - * @param array $props - * @return static - */ - public static function create(array $props) - { - // clean up the slug - $props['slug'] = Str::slug($props['slug'] ?? $props['content']['title'] ?? null); - $props['template'] = $props['model'] = strtolower($props['template'] ?? 'default'); - $props['isDraft'] = ($props['draft'] ?? true); - - // create a temporary page object - $page = Page::factory($props); - - // create a form for the page - $form = Form::for($page, [ - 'values' => $props['content'] ?? [] - ]); - - // inject the content - $page = $page->clone(['content' => $form->strings(true)]); - - // run the hooks and creation action - $page = $page->commit('create', ['page' => $page, 'input' => $props], function ($page, $props) { - - // always create pages in the default language - if ($page->kirby()->multilang() === true) { - $languageCode = $page->kirby()->defaultLanguage()->code(); - } else { - $languageCode = null; - } - - // write the content file - $page = $page->save($page->content()->toArray(), $languageCode); - - // flush the parent cache to get children and drafts right - if ($page->isDraft() === true) { - $page->parentModel()->drafts()->append($page->id(), $page); - } else { - $page->parentModel()->children()->append($page->id(), $page); - } - - return $page; - }); - - // publish the new page if a number is given - if (isset($props['num']) === true) { - $page = $page->changeStatus('listed', $props['num']); - } - - return $page; - } - - /** - * Creates a child of the current page - * - * @param array $props - * @return static - */ - public function createChild(array $props) - { - $props = array_merge($props, [ - 'url' => null, - 'num' => null, - 'parent' => $this, - 'site' => $this->site(), - ]); - - $modelClass = Page::$models[$props['template']] ?? Page::class; - return $modelClass::create($props); - } - - /** - * Create the sorting number for the page - * depending on the blueprint settings - * - * @param int|null $num - * @return int - */ - public function createNum(int $num = null): int - { - $mode = $this->blueprint()->num(); - - switch ($mode) { - case 'zero': - return 0; - case 'date': - case 'datetime': - // the $format needs to produce only digits, - // so it can be converted to integer below - $format = $mode === 'date' ? 'Ymd' : 'YmdHi'; - $lang = $this->kirby()->defaultLanguage() ?? null; - $field = $this->content($lang)->get('date'); - $date = $field->isEmpty() ? 'now' : $field; - return (int)date($format, strtotime($date)); - case 'default': - - $max = $this - ->parentModel() - ->children() - ->listed() - ->merge($this) - ->count(); - - // default positioning at the end - if ($num === null) { - $num = $max; - } - - // avoid zeros or negative numbers - if ($num < 1) { - return 1; - } - - // avoid higher numbers than possible - if ($num > $max) { - return $max; - } - - return $num; - default: - // get instance with default language - $app = $this->kirby()->clone([], false); - $app->setCurrentLanguage(); - - $template = Str::template($mode, [ - 'kirby' => $app, - 'page' => $app->page($this->id()), - 'site' => $app->site(), - ], ['fallback' => '']); - - return (int)$template; - } - } - - /** - * Deletes the page - * - * @param bool $force - * @return bool - */ - public function delete(bool $force = false): bool - { - return $this->commit('delete', ['page' => $this, 'force' => $force], function ($page, $force) { - - // delete all files individually - foreach ($page->files() as $file) { - $file->delete(); - } - - // delete all children individually - foreach ($page->children() as $child) { - $child->delete(true); - } - - // actually remove the page from disc - if ($page->exists() === true) { - - // delete all public media files - Dir::remove($page->mediaRoot()); - - // delete the content folder for this page - Dir::remove($page->root()); - - // if the page is a draft and the _drafts folder - // is now empty. clean it up. - if ($page->isDraft() === true) { - $draftsDir = dirname($page->root()); - - if (Dir::isEmpty($draftsDir) === true) { - Dir::remove($draftsDir); - } - } - } - - if ($page->isDraft() === true) { - $page->parentModel()->drafts()->remove($page); - } else { - $page->parentModel()->children()->remove($page); - $page->resortSiblingsAfterUnlisting(); - } - - return true; - }); - } - - /** - * Duplicates the page with the given - * slug and optionally copies all files - * - * @param string|null $slug - * @param array $options - * @return \Kirby\Cms\Page - */ - public function duplicate(string $slug = null, array $options = []) - { - - // create the slug for the duplicate - $slug = Str::slug($slug ?? $this->slug() . '-' . Str::slug(t('page.duplicate.appendix'))); - - $arguments = [ - 'originalPage' => $this, - 'input' => $slug, - 'options' => $options - ]; - - return $this->commit('duplicate', $arguments, function ($page, $slug, $options) { - $page = $this->copy([ - 'parent' => $this->parent(), - 'slug' => $slug, - 'isDraft' => true, - 'files' => $options['files'] ?? false, - 'children' => $options['children'] ?? false, - ]); - - if (isset($options['title']) === true) { - $page = $page->changeTitle($options['title']); - } - - return $page; - }); - } - - /** - * @return $this|static - * @throws \Kirby\Exception\LogicException If the folder cannot be moved - */ - public function publish() - { - if ($this->isDraft() === false) { - return $this; - } - - $page = $this->clone([ - 'isDraft' => false, - 'root' => null - ]); - - // actually do it on disk - if ($this->exists() === true) { - if (Dir::move($this->root(), $page->root()) !== true) { - throw new LogicException('The draft folder cannot be moved'); - } - - // Get the draft folder and check if there are any other drafts - // left. Otherwise delete it. - $draftDir = dirname($this->root()); - - if (Dir::isEmpty($draftDir) === true) { - Dir::remove($draftDir); - } - } - - // remove the page from the parent drafts and add it to children - $page->parentModel()->drafts()->remove($page); - $page->parentModel()->children()->append($page->id(), $page); - - return $page; - } - - /** - * Clean internal caches - * @return $this - */ - public function purge() - { - $this->blueprint = null; - $this->children = null; - $this->content = null; - $this->drafts = null; - $this->files = null; - $this->inventory = null; - $this->translations = null; - - return $this; - } - - /** - * @param int|null $position - * @return bool - * @throws \Kirby\Exception\LogicException If the page is not included in the siblings collection - */ - protected function resortSiblingsAfterListing(int $position = null): bool - { - // get all siblings including the current page - $siblings = $this - ->parentModel() - ->children() - ->listed() - ->append($this) - ->filter(fn ($page) => $page->blueprint()->num() === 'default'); - - // get a non-associative array of ids - $keys = $siblings->keys(); - $index = array_search($this->id(), $keys); - - // if the page is not included in the siblings something went wrong - if ($index === false) { - throw new LogicException('The page is not included in the sorting index'); - } - - if ($position > count($keys)) { - $position = count($keys); - } - - // move the current page number in the array of keys - // subtract 1 from the num and the position, because of the - // zero-based array keys - $sorted = A::move($keys, $index, $position - 1); - - foreach ($sorted as $key => $id) { - if ($id === $this->id()) { - continue; - } elseif ($sibling = $siblings->get($id)) { - $sibling->changeNum($key + 1); - } - } - - $parent = $this->parentModel(); - $parent->children = $parent->children()->sort('num', 'asc'); - - return true; - } - - /** - * @return bool - */ - public function resortSiblingsAfterUnlisting(): bool - { - $index = 0; - $parent = $this->parentModel(); - $siblings = $parent - ->children() - ->listed() - ->not($this) - ->filter(fn ($page) => $page->blueprint()->num() === 'default'); - - if ($siblings->count() > 0) { - foreach ($siblings as $sibling) { - $index++; - $sibling->changeNum($index); - } - - $parent->children = $siblings->sort('num', 'asc'); - } - - return true; - } - - /** - * Convert a page from listed or - * unlisted to draft. - * - * @return $this|static - * @throws \Kirby\Exception\LogicException If the folder cannot be moved - */ - public function unpublish() - { - if ($this->isDraft() === true) { - return $this; - } - - $page = $this->clone([ - 'isDraft' => true, - 'num' => null, - 'dirname' => null, - 'root' => null - ]); - - // actually do it on disk - if ($this->exists() === true) { - if (Dir::move($this->root(), $page->root()) !== true) { - throw new LogicException('The page folder cannot be moved to drafts'); - } - } - - // remove the page from the parent children and add it to drafts - $page->parentModel()->children()->remove($page); - $page->parentModel()->drafts()->append($page->id(), $page); - - $page->resortSiblingsAfterUnlisting(); - - return $page; - } - - /** - * Updates the page data - * - * @param array|null $input - * @param string|null $languageCode - * @param bool $validate - * @return static - */ - public function update(array $input = null, string $languageCode = null, bool $validate = false) - { - if ($this->isDraft() === true) { - $validate = false; - } - - $page = parent::update($input, $languageCode, $validate); - - // if num is created from page content, update num on content update - if ($page->isListed() === true && in_array($page->blueprint()->num(), ['zero', 'default']) === false) { - $page = $page->changeNum($page->createNum()); - } - - return $page; - } + /** + * Changes the sorting number. + * The sorting number must already be correct + * when the method is called. + * This only affects this page, + * siblings will not be resorted. + * + * @param int|null $num + * @return $this|static + * @throws \Kirby\Exception\LogicException If a draft is being sorted or the directory cannot be moved + */ + public function changeNum(int $num = null) + { + if ($this->isDraft() === true) { + throw new LogicException('Drafts cannot change their sorting number'); + } + + // don't run the action if everything stayed the same + if ($this->num() === $num) { + return $this; + } + + return $this->commit('changeNum', ['page' => $this, 'num' => $num], function ($oldPage, $num) { + $newPage = $oldPage->clone([ + 'num' => $num, + 'dirname' => null, + 'root' => null + ]); + + // actually move the page on disk + if ($oldPage->exists() === true) { + if (Dir::move($oldPage->root(), $newPage->root()) === true) { + // Updates the root path of the old page with the root path + // of the moved new page to use fly actions on old page in loop + $oldPage->setRoot($newPage->root()); + } else { + throw new LogicException('The page directory cannot be moved'); + } + } + + // overwrite the child in the parent page + static::updateParentCollections($newPage, 'set'); + + return $newPage; + }); + } + + /** + * Changes the slug/uid of the page + * + * @param string $slug + * @param string|null $languageCode + * @return $this|static + * @throws \Kirby\Exception\LogicException If the directory cannot be moved + */ + public function changeSlug(string $slug, string $languageCode = null) + { + // always sanitize the slug + $slug = Str::slug($slug); + + // in multi-language installations the slug for the non-default + // languages is stored in the text file. The changeSlugForLanguage + // method takes care of that. + if ($language = $this->kirby()->language($languageCode)) { + if ($language->isDefault() === false) { + return $this->changeSlugForLanguage($slug, $languageCode); + } + } + + // if the slug stays exactly the same, + // nothing needs to be done. + if ($slug === $this->slug()) { + return $this; + } + + $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => null]; + return $this->commit('changeSlug', $arguments, function ($oldPage, $slug) { + $newPage = $oldPage->clone([ + 'slug' => $slug, + 'dirname' => null, + 'root' => null + ]); + + if ($oldPage->exists() === true) { + // remove the lock of the old page + if ($lock = $oldPage->lock()) { + $lock->remove(); + } + + // actually move stuff on disk + if (Dir::move($oldPage->root(), $newPage->root()) !== true) { + throw new LogicException('The page directory cannot be moved'); + } + + // remove from the siblings + static::updateParentCollections($oldPage, 'remove'); + + Dir::remove($oldPage->mediaRoot()); + } + + // overwrite the new page in the parent collection + static::updateParentCollections($newPage, 'set'); + + return $newPage; + }); + } + + /** + * Change the slug for a specific language + * + * @param string $slug + * @param string|null $languageCode + * @return static + * @throws \Kirby\Exception\NotFoundException If the language for the given language code cannot be found + * @throws \Kirby\Exception\InvalidArgumentException If the slug for the default language is being changed + */ + protected function changeSlugForLanguage(string $slug, string $languageCode = null) + { + $language = $this->kirby()->language($languageCode); + + if (!$language) { + throw new NotFoundException('The language: "' . $languageCode . '" does not exist'); + } + + if ($language->isDefault() === true) { + throw new InvalidArgumentException('Use the changeSlug method to change the slug for the default language'); + } + + $arguments = ['page' => $this, 'slug' => $slug, 'languageCode' => $languageCode]; + return $this->commit('changeSlug', $arguments, function ($page, $slug, $languageCode) { + // remove the slug if it's the same as the folder name + if ($slug === $page->uid()) { + $slug = null; + } + + $newPage = $page->save(['slug' => $slug], $languageCode); + + // overwrite the updated page in the parent collection + static::updateParentCollections($newPage, 'set'); + + return $newPage; + }); + } + + /** + * Change the status of the current page + * to either draft, listed or unlisted. + * If changing to `listed`, you can pass a position for the + * page in the siblings collection. Siblings will be resorted. + * + * @param string $status "draft", "listed" or "unlisted" + * @param int|null $position Optional sorting number + * @return static + * @throws \Kirby\Exception\InvalidArgumentException If an invalid status is being passed + */ + public function changeStatus(string $status, int $position = null) + { + switch ($status) { + case 'draft': + return $this->changeStatusToDraft(); + case 'listed': + return $this->changeStatusToListed($position); + case 'unlisted': + return $this->changeStatusToUnlisted(); + default: + throw new InvalidArgumentException('Invalid status: ' . $status); + } + } + + /** + * @return static + */ + protected function changeStatusToDraft() + { + $arguments = ['page' => $this, 'status' => 'draft', 'position' => null]; + $page = $this->commit( + 'changeStatus', + $arguments, + fn ($page) => $page->unpublish() + ); + + return $page; + } + + /** + * @param int|null $position + * @return $this|static + */ + protected function changeStatusToListed(int $position = null) + { + // create a sorting number for the page + $num = $this->createNum($position); + + // don't sort if not necessary + if ($this->status() === 'listed' && $num === $this->num()) { + return $this; + } + + $arguments = ['page' => $this, 'status' => 'listed', 'position' => $num]; + $page = $this->commit('changeStatus', $arguments, function ($page, $status, $position) { + return $page->publish()->changeNum($position); + }); + + if ($this->blueprint()->num() === 'default') { + $page->resortSiblingsAfterListing($num); + } + + return $page; + } + + /** + * @return $this|static + */ + protected function changeStatusToUnlisted() + { + if ($this->status() === 'unlisted') { + return $this; + } + + $arguments = ['page' => $this, 'status' => 'unlisted', 'position' => null]; + $page = $this->commit('changeStatus', $arguments, function ($page) { + return $page->publish()->changeNum(null); + }); + + $this->resortSiblingsAfterUnlisting(); + + return $page; + } + + /** + * Change the position of the page in its siblings + * collection. Siblings will be resorted. If the page + * status isn't yet `listed`, it will be changed to it. + * + * @param int|null $position + * @return $this|static + */ + public function changeSort(int $position = null) + { + return $this->changeStatus('listed', $position); + } + + /** + * Changes the page template + * + * @param string $template + * @return $this|static + * @throws \Kirby\Exception\LogicException If the textfile cannot be renamed/moved + */ + public function changeTemplate(string $template) + { + if ($template === $this->intendedTemplate()->name()) { + return $this; + } + + return $this->commit('changeTemplate', ['page' => $this, 'template' => $template], function ($oldPage, $template) { + if ($this->kirby()->multilang() === true) { + $newPage = $this->clone([ + 'template' => $template + ]); + + foreach ($this->kirby()->languages()->codes() as $code) { + if ($oldPage->translation($code)->exists() !== true) { + continue; + } + + $content = $oldPage->content($code)->convertTo($template); + + if (F::remove($oldPage->contentFile($code)) !== true) { + throw new LogicException('The old text file could not be removed'); + } + + // save the language file + $newPage->save($content, $code); + } + + // return a fresh copy of the object + $page = $newPage->clone(); + } else { + $newPage = $this->clone([ + 'content' => $this->content()->convertTo($template), + 'template' => $template + ]); + + if (F::remove($oldPage->contentFile()) !== true) { + throw new LogicException('The old text file could not be removed'); + } + + $page = $newPage->save(); + } + + // update the parent collection + static::updateParentCollections($page, 'set'); + + return $page; + }); + } + + /** + * Change the page title + * + * @param string $title + * @param string|null $languageCode + * @return static + */ + public function changeTitle(string $title, string $languageCode = null) + { + $arguments = ['page' => $this, 'title' => $title, 'languageCode' => $languageCode]; + return $this->commit('changeTitle', $arguments, function ($page, $title, $languageCode) { + $page = $page->save(['title' => $title], $languageCode); + + // flush the parent cache to get children and drafts right + static::updateParentCollections($page, 'set'); + + return $page; + }); + } + + /** + * Commits a page action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the store action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param array $arguments + * @param \Closure $callback + * @return mixed + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); + + $this->rules()->$action(...$argumentValues); + $kirby->trigger('page.' . $action . ':before', $arguments); + + $result = $callback(...$argumentValues); + + if ($action === 'create') { + $argumentsAfter = ['page' => $result]; + } elseif ($action === 'duplicate') { + $argumentsAfter = ['duplicatePage' => $result, 'originalPage' => $old]; + } elseif ($action === 'delete') { + $argumentsAfter = ['status' => $result, 'page' => $old]; + } else { + $argumentsAfter = ['newPage' => $result, 'oldPage' => $old]; + } + $kirby->trigger('page.' . $action . ':after', $argumentsAfter); + + $kirby->cache('pages')->flush(); + return $result; + } + + /** + * Copies the page to a new parent + * + * @param array $options + * @return \Kirby\Cms\Page + * @throws \Kirby\Exception\DuplicateException If the page already exists + */ + public function copy(array $options = []) + { + $slug = $options['slug'] ?? $this->slug(); + $isDraft = $options['isDraft'] ?? $this->isDraft(); + $parent = $options['parent'] ?? null; + $parentModel = $options['parent'] ?? $this->site(); + $num = $options['num'] ?? null; + $children = $options['children'] ?? false; + $files = $options['files'] ?? false; + + // clean up the slug + $slug = Str::slug($slug); + + if ($parentModel->findPageOrDraft($slug)) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + + $tmp = new static([ + 'isDraft' => $isDraft, + 'num' => $num, + 'parent' => $parent, + 'slug' => $slug, + ]); + + $ignore = [ + $this->kirby()->locks()->file($this) + ]; + + // don't copy files + if ($files === false) { + foreach ($this->files() as $file) { + $ignore[] = $file->root(); + + // append all content files + array_push($ignore, ...$file->contentFiles()); + } + } + + Dir::copy($this->root(), $tmp->root(), $children, $ignore); + + $copy = $parentModel->clone()->findPageOrDraft($slug); + + // remove all translated slugs + if ($this->kirby()->multilang() === true) { + foreach ($this->kirby()->languages() as $language) { + if ($language->isDefault() === false && $copy->translation($language)->exists() === true) { + $copy = $copy->save(['slug' => null], $language->code()); + } + } + } + + // add copy to siblings + static::updateParentCollections($copy, 'append', $parentModel); + + return $copy; + } + + /** + * Creates and stores a new page + * + * @param array $props + * @return static + */ + public static function create(array $props) + { + // clean up the slug + $props['slug'] = Str::slug($props['slug'] ?? $props['content']['title'] ?? null); + $props['template'] = $props['model'] = strtolower($props['template'] ?? 'default'); + $props['isDraft'] = ($props['draft'] ?? true); + + // create a temporary page object + $page = Page::factory($props); + + // create a form for the page + $form = Form::for($page, [ + 'values' => $props['content'] ?? [] + ]); + + // inject the content + $page = $page->clone(['content' => $form->strings(true)]); + + // run the hooks and creation action + $page = $page->commit('create', ['page' => $page, 'input' => $props], function ($page, $props) { + // always create pages in the default language + if ($page->kirby()->multilang() === true) { + $languageCode = $page->kirby()->defaultLanguage()->code(); + } else { + $languageCode = null; + } + + // write the content file + $page = $page->save($page->content()->toArray(), $languageCode); + + // flush the parent cache to get children and drafts right + static::updateParentCollections($page, 'append'); + + return $page; + }); + + // publish the new page if a number is given + if (isset($props['num']) === true) { + $page = $page->changeStatus('listed', $props['num']); + } + + return $page; + } + + /** + * Creates a child of the current page + * + * @param array $props + * @return static + */ + public function createChild(array $props) + { + $props = array_merge($props, [ + 'url' => null, + 'num' => null, + 'parent' => $this, + 'site' => $this->site(), + ]); + + $modelClass = Page::$models[$props['template']] ?? Page::class; + return $modelClass::create($props); + } + + /** + * Create the sorting number for the page + * depending on the blueprint settings + * + * @param int|null $num + * @return int + */ + public function createNum(int $num = null): int + { + $mode = $this->blueprint()->num(); + + switch ($mode) { + case 'zero': + return 0; + case 'date': + case 'datetime': + // the $format needs to produce only digits, + // so it can be converted to integer below + $format = $mode === 'date' ? 'Ymd' : 'YmdHi'; + $lang = $this->kirby()->defaultLanguage() ?? null; + $field = $this->content($lang)->get('date'); + $date = $field->isEmpty() ? 'now' : $field; + return (int)date($format, strtotime($date)); + case 'default': + + $max = $this + ->parentModel() + ->children() + ->listed() + ->merge($this) + ->count(); + + // default positioning at the end + if ($num === null) { + $num = $max; + } + + // avoid zeros or negative numbers + if ($num < 1) { + return 1; + } + + // avoid higher numbers than possible + if ($num > $max) { + return $max; + } + + return $num; + default: + // get instance with default language + $app = $this->kirby()->clone([], false); + $app->setCurrentLanguage(); + + $template = Str::template($mode, [ + 'kirby' => $app, + 'page' => $app->page($this->id()), + 'site' => $app->site(), + ], ['fallback' => '']); + + return (int)$template; + } + } + + /** + * Deletes the page + * + * @param bool $force + * @return bool + */ + public function delete(bool $force = false): bool + { + return $this->commit('delete', ['page' => $this, 'force' => $force], function ($page, $force) { + // delete all files individually + foreach ($page->files() as $file) { + $file->delete(); + } + + // delete all children individually + foreach ($page->children() as $child) { + $child->delete(true); + } + + // actually remove the page from disc + if ($page->exists() === true) { + // delete all public media files + Dir::remove($page->mediaRoot()); + + // delete the content folder for this page + Dir::remove($page->root()); + + // if the page is a draft and the _drafts folder + // is now empty. clean it up. + if ($page->isDraft() === true) { + $draftsDir = dirname($page->root()); + + if (Dir::isEmpty($draftsDir) === true) { + Dir::remove($draftsDir); + } + } + } + + static::updateParentCollections($page, 'remove'); + + if ($page->isDraft() === false) { + $page->resortSiblingsAfterUnlisting(); + } + + return true; + }); + } + + /** + * Duplicates the page with the given + * slug and optionally copies all files + * + * @param string|null $slug + * @param array $options + * @return \Kirby\Cms\Page + */ + public function duplicate(string $slug = null, array $options = []) + { + // create the slug for the duplicate + $slug = Str::slug($slug ?? $this->slug() . '-' . Str::slug(I18n::translate('page.duplicate.appendix'))); + + $arguments = [ + 'originalPage' => $this, + 'input' => $slug, + 'options' => $options + ]; + + return $this->commit('duplicate', $arguments, function ($page, $slug, $options) { + $page = $this->copy([ + 'parent' => $this->parent(), + 'slug' => $slug, + 'isDraft' => true, + 'files' => $options['files'] ?? false, + 'children' => $options['children'] ?? false, + ]); + + if (isset($options['title']) === true) { + $page = $page->changeTitle($options['title']); + } + + return $page; + }); + } + + /** + * @return $this|static + * @throws \Kirby\Exception\LogicException If the folder cannot be moved + */ + public function publish() + { + if ($this->isDraft() === false) { + return $this; + } + + $page = $this->clone([ + 'isDraft' => false, + 'root' => null + ]); + + // actually do it on disk + if ($this->exists() === true) { + if (Dir::move($this->root(), $page->root()) !== true) { + throw new LogicException('The draft folder cannot be moved'); + } + + // Get the draft folder and check if there are any other drafts + // left. Otherwise delete it. + $draftDir = dirname($this->root()); + + if (Dir::isEmpty($draftDir) === true) { + Dir::remove($draftDir); + } + } + + // remove the page from the parent drafts and add it to children + $parentModel = $page->parentModel(); + $parentModel->drafts()->remove($page); + $parentModel->children()->append($page->id(), $page); + + // update the childrenAndDrafts() cache if it is initialized + if ($parentModel->childrenAndDrafts !== null) { + $parentModel->childrenAndDrafts()->set($page->id(), $page); + } + + return $page; + } + + /** + * Clean internal caches + * @return $this + */ + public function purge() + { + $this->blueprint = null; + $this->children = null; + $this->childrenAndDrafts = null; + $this->content = null; + $this->drafts = null; + $this->files = null; + $this->inventory = null; + $this->translations = null; + + return $this; + } + + /** + * @param int|null $position + * @return bool + * @throws \Kirby\Exception\LogicException If the page is not included in the siblings collection + */ + protected function resortSiblingsAfterListing(int $position = null): bool + { + // get all siblings including the current page + $siblings = $this + ->parentModel() + ->children() + ->listed() + ->append($this) + ->filter(fn ($page) => $page->blueprint()->num() === 'default'); + + // get a non-associative array of ids + $keys = $siblings->keys(); + $index = array_search($this->id(), $keys); + + // if the page is not included in the siblings something went wrong + if ($index === false) { + throw new LogicException('The page is not included in the sorting index'); + } + + if ($position > count($keys)) { + $position = count($keys); + } + + // move the current page number in the array of keys + // subtract 1 from the num and the position, because of the + // zero-based array keys + $sorted = A::move($keys, $index, $position - 1); + + foreach ($sorted as $key => $id) { + if ($id === $this->id()) { + continue; + } elseif ($sibling = $siblings->get($id)) { + $sibling->changeNum($key + 1); + } + } + + $parent = $this->parentModel(); + $parent->children = $parent->children()->sort('num', 'asc'); + $parent->childrenAndDrafts = null; + + return true; + } + + /** + * @return bool + */ + public function resortSiblingsAfterUnlisting(): bool + { + $index = 0; + $parent = $this->parentModel(); + $siblings = $parent + ->children() + ->listed() + ->not($this) + ->filter(fn ($page) => $page->blueprint()->num() === 'default'); + + if ($siblings->count() > 0) { + foreach ($siblings as $sibling) { + $index++; + $sibling->changeNum($index); + } + + $parent->children = $siblings->sort('num', 'asc'); + $parent->childrenAndDrafts = null; + } + + return true; + } + + /** + * Convert a page from listed or + * unlisted to draft. + * + * @return $this|static + * @throws \Kirby\Exception\LogicException If the folder cannot be moved + */ + public function unpublish() + { + if ($this->isDraft() === true) { + return $this; + } + + $page = $this->clone([ + 'isDraft' => true, + 'num' => null, + 'dirname' => null, + 'root' => null + ]); + + // actually do it on disk + if ($this->exists() === true) { + if (Dir::move($this->root(), $page->root()) !== true) { + throw new LogicException('The page folder cannot be moved to drafts'); + } + } + + // remove the page from the parent children and add it to drafts + $parentModel = $page->parentModel(); + $parentModel->children()->remove($page); + $parentModel->drafts()->append($page->id(), $page); + + // update the childrenAndDrafts() cache if it is initialized + if ($parentModel->childrenAndDrafts !== null) { + $parentModel->childrenAndDrafts()->set($page->id(), $page); + } + + $page->resortSiblingsAfterUnlisting(); + + return $page; + } + + /** + * Updates the page data + * + * @param array|null $input + * @param string|null $languageCode + * @param bool $validate + * @return static + */ + public function update(array $input = null, string $languageCode = null, bool $validate = false) + { + if ($this->isDraft() === true) { + $validate = false; + } + + $page = parent::update($input, $languageCode, $validate); + + // if num is created from page content, update num on content update + if ($page->isListed() === true && in_array($page->blueprint()->num(), ['zero', 'default']) === false) { + $page = $page->changeNum($page->createNum()); + } + + // overwrite the updated page in the parent collection + static::updateParentCollections($page, 'set'); + + return $page; + } + + /** + * Updates parent collections with the new page object + * after a page action + * + * @param \Kirby\Cms\Page $page + * @param string $method Method to call on the parent collections + * @param \Kirby\Cms\Page|null $parentMdel + * @return void + */ + protected static function updateParentCollections($page, string $method, $parentModel = null): void + { + $parentModel ??= $page->parentModel(); + + // method arguments depending on the called method + $args = $method === 'remove' ? [$page] : [$page->id(), $page]; + + if ($page->isDraft() === true) { + $parentModel->drafts()->$method(...$args); + } else { + $parentModel->children()->$method(...$args); + } + + // update the childrenAndDrafts() cache if it is initialized + if ($parentModel->childrenAndDrafts !== null) { + $parentModel->childrenAndDrafts()->$method(...$args); + } + } } diff --git a/kirby/src/Cms/PageBlueprint.php b/kirby/src/Cms/PageBlueprint.php index 09a2584..87e4937 100644 --- a/kirby/src/Cms/PageBlueprint.php +++ b/kirby/src/Cms/PageBlueprint.php @@ -13,197 +13,196 @@ namespace Kirby\Cms; */ class PageBlueprint extends Blueprint { - /** - * Creates a new page blueprint object - * with the given props - * - * @param array $props - */ - public function __construct(array $props) - { - parent::__construct($props); + /** + * Creates a new page blueprint object + * with the given props + * + * @param array $props + */ + public function __construct(array $props) + { + parent::__construct($props); - // normalize all available page options - $this->props['options'] = $this->normalizeOptions( - $props['options'] ?? true, - // defaults - [ - 'changeSlug' => null, - 'changeStatus' => null, - 'changeTemplate' => null, - 'changeTitle' => null, - 'create' => null, - 'delete' => null, - 'duplicate' => null, - 'read' => null, - 'preview' => null, - 'sort' => null, - 'update' => null, - ], - // aliases (from v2) - [ - 'status' => 'changeStatus', - 'template' => 'changeTemplate', - 'title' => 'changeTitle', - 'url' => 'changeSlug', - ] - ); + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $this->props['options'] ?? true, + // defaults + [ + 'changeSlug' => null, + 'changeStatus' => null, + 'changeTemplate' => null, + 'changeTitle' => null, + 'create' => null, + 'delete' => null, + 'duplicate' => null, + 'read' => null, + 'preview' => null, + 'sort' => null, + 'update' => null, + ], + // aliases (from v2) + [ + 'status' => 'changeStatus', + 'template' => 'changeTemplate', + 'title' => 'changeTitle', + 'url' => 'changeSlug', + ] + ); - // normalize the ordering number - $this->props['num'] = $this->normalizeNum($props['num'] ?? 'default'); + // normalize the ordering number + $this->props['num'] = $this->normalizeNum($this->props['num'] ?? 'default'); - // normalize the available status array - $this->props['status'] = $this->normalizeStatus($props['status'] ?? null); - } + // normalize the available status array + $this->props['status'] = $this->normalizeStatus($this->props['status'] ?? null); + } - /** - * Returns the page numbering mode - * - * @return string - */ - public function num(): string - { - return $this->props['num']; - } + /** + * Returns the page numbering mode + * + * @return string + */ + public function num(): string + { + return $this->props['num']; + } - /** - * Normalizes the ordering number - * - * @param mixed $num - * @return string - */ - protected function normalizeNum($num): string - { - $aliases = [ - '0' => 'zero', - 'sort' => 'default', - ]; + /** + * Normalizes the ordering number + * + * @param mixed $num + * @return string + */ + protected function normalizeNum($num): string + { + $aliases = [ + '0' => 'zero', + 'sort' => 'default', + ]; - if (isset($aliases[$num]) === true) { - return $aliases[$num]; - } + if (isset($aliases[$num]) === true) { + return $aliases[$num]; + } - return $num; - } + return $num; + } - /** - * Normalizes the available status options for the page - * - * @param mixed $status - * @return array - */ - protected function normalizeStatus($status): array - { - $defaults = [ - 'draft' => [ - 'label' => $this->i18n('page.status.draft'), - 'text' => $this->i18n('page.status.draft.description'), - ], - 'unlisted' => [ - 'label' => $this->i18n('page.status.unlisted'), - 'text' => $this->i18n('page.status.unlisted.description'), - ], - 'listed' => [ - 'label' => $this->i18n('page.status.listed'), - 'text' => $this->i18n('page.status.listed.description'), - ] - ]; + /** + * Normalizes the available status options for the page + * + * @param mixed $status + * @return array + */ + protected function normalizeStatus($status): array + { + $defaults = [ + 'draft' => [ + 'label' => $this->i18n('page.status.draft'), + 'text' => $this->i18n('page.status.draft.description'), + ], + 'unlisted' => [ + 'label' => $this->i18n('page.status.unlisted'), + 'text' => $this->i18n('page.status.unlisted.description'), + ], + 'listed' => [ + 'label' => $this->i18n('page.status.listed'), + 'text' => $this->i18n('page.status.listed.description'), + ] + ]; - // use the defaults, when the status is not defined - if (empty($status) === true) { - $status = $defaults; - } + // use the defaults, when the status is not defined + if (empty($status) === true) { + $status = $defaults; + } - // extend the status definition - $status = $this->extend($status); + // extend the status definition + $status = $this->extend($status); - // clean up and translate each status - foreach ($status as $key => $options) { + // clean up and translate each status + foreach ($status as $key => $options) { + // skip invalid status definitions + if (in_array($key, ['draft', 'listed', 'unlisted']) === false || $options === false) { + unset($status[$key]); + continue; + } - // skip invalid status definitions - if (in_array($key, ['draft', 'listed', 'unlisted']) === false || $options === false) { - unset($status[$key]); - continue; - } + if ($options === true) { + $status[$key] = $defaults[$key]; + continue; + } - if ($options === true) { - $status[$key] = $defaults[$key]; - continue; - } + // convert everything to a simple array + if (is_array($options) === false) { + $status[$key] = [ + 'label' => $options, + 'text' => null + ]; + } - // convert everything to a simple array - if (is_array($options) === false) { - $status[$key] = [ - 'label' => $options, - 'text' => null - ]; - } + // always make sure to have a proper label + if (empty($status[$key]['label']) === true) { + $status[$key]['label'] = $defaults[$key]['label']; + } - // always make sure to have a proper label - if (empty($status[$key]['label']) === true) { - $status[$key]['label'] = $defaults[$key]['label']; - } + // also make sure to have the text field set + if (isset($status[$key]['text']) === false) { + $status[$key]['text'] = null; + } - // also make sure to have the text field set - if (isset($status[$key]['text']) === false) { - $status[$key]['text'] = null; - } + // translate text and label if necessary + $status[$key]['label'] = $this->i18n($status[$key]['label'], $status[$key]['label']); + $status[$key]['text'] = $this->i18n($status[$key]['text'], $status[$key]['text']); + } - // translate text and label if necessary - $status[$key]['label'] = $this->i18n($status[$key]['label'], $status[$key]['label']); - $status[$key]['text'] = $this->i18n($status[$key]['text'], $status[$key]['text']); - } + // the draft status is required + if (isset($status['draft']) === false) { + $status = ['draft' => $defaults['draft']] + $status; + } - // the draft status is required - if (isset($status['draft']) === false) { - $status = ['draft' => $defaults['draft']] + $status; - } + // remove the draft status for the home and error pages + if ($this->model->isHomeOrErrorPage() === true) { + unset($status['draft']); + } - // remove the draft status for the home and error pages - if ($this->model->isHomeOrErrorPage() === true) { - unset($status['draft']); - } + return $status; + } - return $status; - } + /** + * Returns the options object + * that handles page options and permissions + * + * @return array + */ + public function options(): array + { + return $this->props['options']; + } - /** - * Returns the options object - * that handles page options and permissions - * - * @return array - */ - public function options(): array - { - return $this->props['options']; - } + /** + * Returns the preview settings + * The preview setting controls the "Open" + * button in the panel and redirects it to a + * different URL if necessary. + * + * @return string|bool + */ + public function preview() + { + $preview = $this->props['options']['preview'] ?? true; - /** - * Returns the preview settings - * The preview setting controls the "Open" - * button in the panel and redirects it to a - * different URL if necessary. - * - * @return string|bool - */ - public function preview() - { - $preview = $this->props['options']['preview'] ?? true; + if (is_string($preview) === true) { + return $this->model->toString($preview); + } - if (is_string($preview) === true) { - return $this->model->toString($preview); - } + return $preview; + } - return $preview; - } - - /** - * Returns the status array - * - * @return array - */ - public function status(): array - { - return $this->props['status']; - } + /** + * Returns the status array + * + * @return array + */ + public function status(): array + { + return $this->props['status']; + } } diff --git a/kirby/src/Cms/PagePermissions.php b/kirby/src/Cms/PagePermissions.php index 9943235..53fcb84 100644 --- a/kirby/src/Cms/PagePermissions.php +++ b/kirby/src/Cms/PagePermissions.php @@ -13,68 +13,68 @@ namespace Kirby\Cms; */ class PagePermissions extends ModelPermissions { - /** - * @var string - */ - protected $category = 'pages'; + /** + * @var string + */ + protected $category = 'pages'; - /** - * @return bool - */ - protected function canChangeSlug(): bool - { - return $this->model->isHomeOrErrorPage() !== true; - } + /** + * @return bool + */ + protected function canChangeSlug(): bool + { + return $this->model->isHomeOrErrorPage() !== true; + } - /** - * @return bool - */ - protected function canChangeStatus(): bool - { - return $this->model->isErrorPage() !== true; - } + /** + * @return bool + */ + protected function canChangeStatus(): bool + { + return $this->model->isErrorPage() !== true; + } - /** - * @return bool - */ - protected function canChangeTemplate(): bool - { - if ($this->model->isHomeOrErrorPage() === true) { - return false; - } + /** + * @return bool + */ + protected function canChangeTemplate(): bool + { + if ($this->model->isHomeOrErrorPage() === true) { + return false; + } - if (count($this->model->blueprints()) <= 1) { - return false; - } + if (count($this->model->blueprints()) <= 1) { + return false; + } - return true; - } + return true; + } - /** - * @return bool - */ - protected function canDelete(): bool - { - return $this->model->isHomeOrErrorPage() !== true; - } + /** + * @return bool + */ + protected function canDelete(): bool + { + return $this->model->isHomeOrErrorPage() !== true; + } - /** - * @return bool - */ - protected function canSort(): bool - { - if ($this->model->isErrorPage() === true) { - return false; - } + /** + * @return bool + */ + protected function canSort(): bool + { + if ($this->model->isErrorPage() === true) { + return false; + } - if ($this->model->isListed() !== true) { - return false; - } + if ($this->model->isListed() !== true) { + return false; + } - if ($this->model->blueprint()->num() !== 'default') { - return false; - } + if ($this->model->blueprint()->num() !== 'default') { + return false; + } - return true; - } + return true; + } } diff --git a/kirby/src/Cms/PagePicker.php b/kirby/src/Cms/PagePicker.php index 6bc74d7..802e7e8 100644 --- a/kirby/src/Cms/PagePicker.php +++ b/kirby/src/Cms/PagePicker.php @@ -18,248 +18,248 @@ use Kirby\Exception\InvalidArgumentException; */ class PagePicker extends Picker { - /** - * @var \Kirby\Cms\Pages - */ - protected $items; + /** + * @var \Kirby\Cms\Pages + */ + protected $items; - /** - * @var \Kirby\Cms\Pages - */ - protected $itemsForQuery; + /** + * @var \Kirby\Cms\Pages + */ + protected $itemsForQuery; - /** - * @var \Kirby\Cms\Page|\Kirby\Cms\Site|null - */ - protected $parent; + /** + * @var \Kirby\Cms\Page|\Kirby\Cms\Site|null + */ + protected $parent; - /** - * Extends the basic defaults - * - * @return array - */ - public function defaults(): array - { - return array_merge(parent::defaults(), [ - // Page ID of the selected parent. Used to navigate - 'parent' => null, - // enable/disable subpage navigation - 'subpages' => true, - ]); - } + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + return array_merge(parent::defaults(), [ + // Page ID of the selected parent. Used to navigate + 'parent' => null, + // enable/disable subpage navigation + 'subpages' => true, + ]); + } - /** - * Returns the parent model object that - * is currently selected in the page picker. - * It normally starts at the site, but can - * also be any subpage. When a query is given - * and subpage navigation is deactivated, - * there will be no model available at all. - * - * @return \Kirby\Cms\Page|\Kirby\Cms\Site|null - */ - public function model() - { - // no subpages navigation = no model - if ($this->options['subpages'] === false) { - return null; - } + /** + * Returns the parent model object that + * is currently selected in the page picker. + * It normally starts at the site, but can + * also be any subpage. When a query is given + * and subpage navigation is deactivated, + * there will be no model available at all. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site|null + */ + public function model() + { + // no subpages navigation = no model + if ($this->options['subpages'] === false) { + return null; + } - // the model for queries is a bit more tricky to find - if (empty($this->options['query']) === false) { - return $this->modelForQuery(); - } + // the model for queries is a bit more tricky to find + if (empty($this->options['query']) === false) { + return $this->modelForQuery(); + } - return $this->parent(); - } + return $this->parent(); + } - /** - * Returns a model object for the given - * query, depending on the parent and subpages - * options. - * - * @return \Kirby\Cms\Page|\Kirby\Cms\Site|null - */ - public function modelForQuery() - { - if ($this->options['subpages'] === true && empty($this->options['parent']) === false) { - return $this->parent(); - } + /** + * Returns a model object for the given + * query, depending on the parent and subpages + * options. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site|null + */ + public function modelForQuery() + { + if ($this->options['subpages'] === true && empty($this->options['parent']) === false) { + return $this->parent(); + } - if ($items = $this->items()) { - return $items->parent(); - } + if ($items = $this->items()) { + return $items->parent(); + } - return null; - } + return null; + } - /** - * Returns basic information about the - * parent model that is currently selected - * in the page picker. - * - * @param \Kirby\Cms\Site|\Kirby\Cms\Page|null - * @return array|null - */ - public function modelToArray($model = null): ?array - { - if ($model === null) { - return null; - } + /** + * Returns basic information about the + * parent model that is currently selected + * in the page picker. + * + * @param \Kirby\Cms\Site|\Kirby\Cms\Page|null + * @return array|null + */ + public function modelToArray($model = null): ?array + { + if ($model === null) { + return null; + } - // the selected model is the site. there's nothing above - if (is_a($model, 'Kirby\Cms\Site') === true) { - return [ - 'id' => null, - 'parent' => null, - 'title' => $model->title()->value() - ]; - } + // the selected model is the site. there's nothing above + if (is_a($model, 'Kirby\Cms\Site') === true) { + return [ + 'id' => null, + 'parent' => null, + 'title' => $model->title()->value() + ]; + } - // the top-most page has been reached - // the missing id indicates that there's nothing above - if ($model->id() === $this->start()->id()) { - return [ - 'id' => null, - 'parent' => null, - 'title' => $model->title()->value() - ]; - } + // the top-most page has been reached + // the missing id indicates that there's nothing above + if ($model->id() === $this->start()->id()) { + return [ + 'id' => null, + 'parent' => null, + 'title' => $model->title()->value() + ]; + } - // the model is a regular page - return [ - 'id' => $model->id(), - 'parent' => $model->parentModel()->id(), - 'title' => $model->title()->value() - ]; - } + // the model is a regular page + return [ + 'id' => $model->id(), + 'parent' => $model->parentModel()->id(), + 'title' => $model->title()->value() + ]; + } - /** - * Search all pages for the picker - * - * @return \Kirby\Cms\Pages|null - */ - public function items() - { - // cache - if ($this->items !== null) { - return $this->items; - } + /** + * Search all pages for the picker + * + * @return \Kirby\Cms\Pages|null + */ + public function items() + { + // cache + if ($this->items !== null) { + return $this->items; + } - // no query? simple parent-based search for pages - if (empty($this->options['query']) === true) { - $items = $this->itemsForParent(); + // no query? simple parent-based search for pages + if (empty($this->options['query']) === true) { + $items = $this->itemsForParent(); - // when subpage navigation is enabled, a parent - // might be passed in addition to the query. - // The parent then takes priority. - } elseif ($this->options['subpages'] === true && empty($this->options['parent']) === false) { - $items = $this->itemsForParent(); + // when subpage navigation is enabled, a parent + // might be passed in addition to the query. + // The parent then takes priority. + } elseif ($this->options['subpages'] === true && empty($this->options['parent']) === false) { + $items = $this->itemsForParent(); - // search by query - } else { - $items = $this->itemsForQuery(); - } + // search by query + } else { + $items = $this->itemsForQuery(); + } - // filter protected pages - $items = $items->filter('isReadable', true); + // filter protected pages + $items = $items->filter('isReadable', true); - // search - $items = $this->search($items); + // search + $items = $this->search($items); - // paginate the result - return $this->items = $this->paginate($items); - } + // paginate the result + return $this->items = $this->paginate($items); + } - /** - * Search for pages by parent - * - * @return \Kirby\Cms\Pages - */ - public function itemsForParent() - { - return $this->parent()->children(); - } + /** + * Search for pages by parent + * + * @return \Kirby\Cms\Pages + */ + public function itemsForParent() + { + return $this->parent()->children(); + } - /** - * Search for pages by query string - * - * @return \Kirby\Cms\Pages - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function itemsForQuery() - { - // cache - if ($this->itemsForQuery !== null) { - return $this->itemsForQuery; - } + /** + * Search for pages by query string + * + * @return \Kirby\Cms\Pages + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function itemsForQuery() + { + // cache + if ($this->itemsForQuery !== null) { + return $this->itemsForQuery; + } - $model = $this->options['model']; - $items = $model->query($this->options['query']); + $model = $this->options['model']; + $items = $model->query($this->options['query']); - // help mitigate some typical query usage issues - // by converting site and page objects to proper - // pages by returning their children - if (is_a($items, 'Kirby\Cms\Site') === true) { - $items = $items->children(); - } elseif (is_a($items, 'Kirby\Cms\Page') === true) { - $items = $items->children(); - } elseif (is_a($items, 'Kirby\Cms\Pages') === false) { - throw new InvalidArgumentException('Your query must return a set of pages'); - } + // help mitigate some typical query usage issues + // by converting site and page objects to proper + // pages by returning their children + if (is_a($items, 'Kirby\Cms\Site') === true) { + $items = $items->children(); + } elseif (is_a($items, 'Kirby\Cms\Page') === true) { + $items = $items->children(); + } elseif (is_a($items, 'Kirby\Cms\Pages') === false) { + throw new InvalidArgumentException('Your query must return a set of pages'); + } - return $this->itemsForQuery = $items; - } + return $this->itemsForQuery = $items; + } - /** - * Returns the parent model. - * The model will be used to fetch - * subpages unless there's a specific - * query to find pages instead. - * - * @return \Kirby\Cms\Page|\Kirby\Cms\Site - */ - public function parent() - { - if ($this->parent !== null) { - return $this->parent; - } + /** + * Returns the parent model. + * The model will be used to fetch + * subpages unless there's a specific + * query to find pages instead. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site + */ + public function parent() + { + if ($this->parent !== null) { + return $this->parent; + } - return $this->parent = $this->kirby->page($this->options['parent']) ?? $this->site; - } + return $this->parent = $this->kirby->page($this->options['parent']) ?? $this->site; + } - /** - * Calculates the top-most model (page or site) - * that can be accessed when navigating - * through pages. - * - * @return \Kirby\Cms\Page|\Kirby\Cms\Site - */ - public function start() - { - if (empty($this->options['query']) === false) { - if ($items = $this->itemsForQuery()) { - return $items->parent(); - } + /** + * Calculates the top-most model (page or site) + * that can be accessed when navigating + * through pages. + * + * @return \Kirby\Cms\Page|\Kirby\Cms\Site + */ + public function start() + { + if (empty($this->options['query']) === false) { + if ($items = $this->itemsForQuery()) { + return $items->parent(); + } - return $this->site; - } + return $this->site; + } - return $this->site; - } + return $this->site; + } - /** - * Returns an associative array - * with all information for the picker. - * This will be passed directly to the API. - * - * @return array - */ - public function toArray(): array - { - $array = parent::toArray(); - $array['model'] = $this->modelToArray($this->model()); + /** + * Returns an associative array + * with all information for the picker. + * This will be passed directly to the API. + * + * @return array + */ + public function toArray(): array + { + $array = parent::toArray(); + $array['model'] = $this->modelToArray($this->model()); - return $array; - } + return $array; + } } diff --git a/kirby/src/Cms/PageRules.php b/kirby/src/Cms/PageRules.php index 4e980a0..68cc711 100644 --- a/kirby/src/Cms/PageRules.php +++ b/kirby/src/Cms/PageRules.php @@ -19,421 +19,421 @@ use Kirby\Toolkit\Str; */ class PageRules { - /** - * Validates if the sorting number of the page can be changed - * - * @param \Kirby\Cms\Page $page - * @param int|null $num - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the given number is invalid - */ - public static function changeNum(Page $page, int $num = null): bool - { - if ($num !== null && $num < 0) { - throw new InvalidArgumentException(['key' => 'page.num.invalid']); - } + /** + * Validates if the sorting number of the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param int|null $num + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the given number is invalid + */ + public static function changeNum(Page $page, int $num = null): bool + { + if ($num !== null && $num < 0) { + throw new InvalidArgumentException(['key' => 'page.num.invalid']); + } - return true; - } + return true; + } - /** - * Validates if the slug for the page can be changed - * - * @param \Kirby\Cms\Page $page - * @param string $slug - * @return bool - * @throws \Kirby\Exception\DuplicateException If a page with this slug already exists - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the slug - */ - public static function changeSlug(Page $page, string $slug): bool - { - if ($page->permissions()->changeSlug() !== true) { - throw new PermissionException([ - 'key' => 'page.changeSlug.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the slug for the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $slug + * @return bool + * @throws \Kirby\Exception\DuplicateException If a page with this slug already exists + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the slug + */ + public static function changeSlug(Page $page, string $slug): bool + { + if ($page->permissions()->changeSlug() !== true) { + throw new PermissionException([ + 'key' => 'page.changeSlug.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - self::validateSlugLength($slug); + self::validateSlugLength($slug); - $siblings = $page->parentModel()->children(); - $drafts = $page->parentModel()->drafts(); + $siblings = $page->parentModel()->children(); + $drafts = $page->parentModel()->drafts(); - if ($duplicate = $siblings->find($slug)) { - if ($duplicate->is($page) === false) { - throw new DuplicateException([ - 'key' => 'page.duplicate', - 'data' => [ - 'slug' => $slug - ] - ]); - } - } + if ($duplicate = $siblings->find($slug)) { + if ($duplicate->is($page) === false) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + } - if ($duplicate = $drafts->find($slug)) { - if ($duplicate->is($page) === false) { - throw new DuplicateException([ - 'key' => 'page.draft.duplicate', - 'data' => [ - 'slug' => $slug - ] - ]); - } - } + if ($duplicate = $drafts->find($slug)) { + if ($duplicate->is($page) === false) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => [ + 'slug' => $slug + ] + ]); + } + } - return true; - } + return true; + } - /** - * Validates if the status for the page can be changed - * - * @param \Kirby\Cms\Page $page - * @param string $status - * @param int|null $position - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the given status is invalid - */ - public static function changeStatus(Page $page, string $status, int $position = null): bool - { - if (isset($page->blueprint()->status()[$status]) === false) { - throw new InvalidArgumentException(['key' => 'page.status.invalid']); - } + /** + * Validates if the status for the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $status + * @param int|null $position + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the given status is invalid + */ + public static function changeStatus(Page $page, string $status, int $position = null): bool + { + if (isset($page->blueprint()->status()[$status]) === false) { + throw new InvalidArgumentException(['key' => 'page.status.invalid']); + } - switch ($status) { - case 'draft': - return static::changeStatusToDraft($page); - case 'listed': - return static::changeStatusToListed($page, $position); - case 'unlisted': - return static::changeStatusToUnlisted($page); - default: - throw new InvalidArgumentException(['key' => 'page.status.invalid']); - } - } + switch ($status) { + case 'draft': + return static::changeStatusToDraft($page); + case 'listed': + return static::changeStatusToListed($page, $position); + case 'unlisted': + return static::changeStatusToUnlisted($page); + default: + throw new InvalidArgumentException(['key' => 'page.status.invalid']); + } + } - /** - * Validates if a page can be converted to a draft - * - * @param \Kirby\Cms\Page $page - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the page cannot be converted to a draft - */ - public static function changeStatusToDraft(Page $page) - { - if ($page->permissions()->changeStatus() !== true) { - throw new PermissionException([ - 'key' => 'page.changeStatus.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if a page can be converted to a draft + * + * @param \Kirby\Cms\Page $page + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the page cannot be converted to a draft + */ + public static function changeStatusToDraft(Page $page) + { + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - if ($page->isHomeOrErrorPage() === true) { - throw new PermissionException([ - 'key' => 'page.changeStatus.toDraft.invalid', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + if ($page->isHomeOrErrorPage() === true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.toDraft.invalid', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - return true; - } + return true; + } - /** - * Validates if the status of a page can be changed to listed - * - * @param \Kirby\Cms\Page $page - * @param int $position - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the given position is invalid - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the status for the page cannot be changed by any user - */ - public static function changeStatusToListed(Page $page, int $position) - { - // no need to check for status changing permissions, - // instead we need to check for sorting permissions - if ($page->isListed() === true) { - if ($page->isSortable() !== true) { - throw new PermissionException([ - 'key' => 'page.sort.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the status of a page can be changed to listed + * + * @param \Kirby\Cms\Page $page + * @param int $position + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the given position is invalid + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status or the status for the page cannot be changed by any user + */ + public static function changeStatusToListed(Page $page, int $position) + { + // no need to check for status changing permissions, + // instead we need to check for sorting permissions + if ($page->isListed() === true) { + if ($page->isSortable() !== true) { + throw new PermissionException([ + 'key' => 'page.sort.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - return true; - } + return true; + } - static::publish($page); + static::publish($page); - if ($position !== null && $position < 0) { - throw new InvalidArgumentException(['key' => 'page.num.invalid']); - } + if ($position !== null && $position < 0) { + throw new InvalidArgumentException(['key' => 'page.num.invalid']); + } - return true; - } + return true; + } - /** - * Validates if the status of a page can be changed to unlisted - * - * @param \Kirby\Cms\Page $page - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status - */ - public static function changeStatusToUnlisted(Page $page) - { - static::publish($page); + /** + * Validates if the status of a page can be changed to unlisted + * + * @param \Kirby\Cms\Page $page + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the status + */ + public static function changeStatusToUnlisted(Page $page) + { + static::publish($page); - return true; - } + return true; + } - /** - * Validates if the template of the page can be changed - * - * @param \Kirby\Cms\Page $page - * @param string $template - * @return bool - * @throws \Kirby\Exception\LogicException If the template of the page cannot be changed at all - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the template - */ - public static function changeTemplate(Page $page, string $template): bool - { - if ($page->permissions()->changeTemplate() !== true) { - throw new PermissionException([ - 'key' => 'page.changeTemplate.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the template of the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $template + * @return bool + * @throws \Kirby\Exception\LogicException If the template of the page cannot be changed at all + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the template + */ + public static function changeTemplate(Page $page, string $template): bool + { + if ($page->permissions()->changeTemplate() !== true) { + throw new PermissionException([ + 'key' => 'page.changeTemplate.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - if (count($page->blueprints()) <= 1) { - throw new LogicException([ - 'key' => 'page.changeTemplate.invalid', - 'data' => ['slug' => $page->slug()] - ]); - } + if (count($page->blueprints()) <= 1) { + throw new LogicException([ + 'key' => 'page.changeTemplate.invalid', + 'data' => ['slug' => $page->slug()] + ]); + } - return true; - } + return true; + } - /** - * Validates if the title of the page can be changed - * - * @param \Kirby\Cms\Page $page - * @param string $title - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the new title is empty - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title - */ - public static function changeTitle(Page $page, string $title): bool - { - if ($page->permissions()->changeTitle() !== true) { - throw new PermissionException([ - 'key' => 'page.changeTitle.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the title of the page can be changed + * + * @param \Kirby\Cms\Page $page + * @param string $title + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the new title is empty + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title + */ + public static function changeTitle(Page $page, string $title): bool + { + if ($page->permissions()->changeTitle() !== true) { + throw new PermissionException([ + 'key' => 'page.changeTitle.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - if (Str::length($title) === 0) { - throw new InvalidArgumentException([ - 'key' => 'page.changeTitle.empty', - ]); - } + if (Str::length($title) === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.changeTitle.empty', + ]); + } - return true; - } + return true; + } - /** - * Validates if the page can be created - * - * @param \Kirby\Cms\Page $page - * @return bool - * @throws \Kirby\Exception\DuplicateException If the same page or a draft already exists - * @throws \Kirby\Exception\InvalidArgumentException If the slug is invalid - * @throws \Kirby\Exception\PermissionException If the user is not allowed to create this page - */ - public static function create(Page $page): bool - { - if ($page->permissions()->create() !== true) { - throw new PermissionException([ - 'key' => 'page.create.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the page can be created + * + * @param \Kirby\Cms\Page $page + * @return bool + * @throws \Kirby\Exception\DuplicateException If the same page or a draft already exists + * @throws \Kirby\Exception\InvalidArgumentException If the slug is invalid + * @throws \Kirby\Exception\PermissionException If the user is not allowed to create this page + */ + public static function create(Page $page): bool + { + if ($page->permissions()->create() !== true) { + throw new PermissionException([ + 'key' => 'page.create.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - self::validateSlugLength($page->slug()); + self::validateSlugLength($page->slug()); - if ($page->exists() === true) { - throw new DuplicateException([ - 'key' => 'page.draft.duplicate', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + if ($page->exists() === true) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - $siblings = $page->parentModel()->children(); - $drafts = $page->parentModel()->drafts(); - $slug = $page->slug(); + $siblings = $page->parentModel()->children(); + $drafts = $page->parentModel()->drafts(); + $slug = $page->slug(); - if ($siblings->find($slug)) { - throw new DuplicateException([ - 'key' => 'page.duplicate', - 'data' => ['slug' => $slug] - ]); - } + if ($siblings->find($slug)) { + throw new DuplicateException([ + 'key' => 'page.duplicate', + 'data' => ['slug' => $slug] + ]); + } - if ($drafts->find($slug)) { - throw new DuplicateException([ - 'key' => 'page.draft.duplicate', - 'data' => ['slug' => $slug] - ]); - } + if ($drafts->find($slug)) { + throw new DuplicateException([ + 'key' => 'page.draft.duplicate', + 'data' => ['slug' => $slug] + ]); + } - return true; - } + return true; + } - /** - * Validates if the page can be deleted - * - * @param \Kirby\Cms\Page $page - * @param bool $force - * @return bool - * @throws \Kirby\Exception\LogicException If the page has children and should not be force-deleted - * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the page - */ - public static function delete(Page $page, bool $force = false): bool - { - if ($page->permissions()->delete() !== true) { - throw new PermissionException([ - 'key' => 'page.delete.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the page can be deleted + * + * @param \Kirby\Cms\Page $page + * @param bool $force + * @return bool + * @throws \Kirby\Exception\LogicException If the page has children and should not be force-deleted + * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete the page + */ + public static function delete(Page $page, bool $force = false): bool + { + if ($page->permissions()->delete() !== true) { + throw new PermissionException([ + 'key' => 'page.delete.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) { - throw new LogicException(['key' => 'page.delete.hasChildren']); - } + if (($page->hasChildren() === true || $page->hasDrafts() === true) && $force === false) { + throw new LogicException(['key' => 'page.delete.hasChildren']); + } - return true; - } + return true; + } - /** - * Validates if the page can be duplicated - * - * @param \Kirby\Cms\Page $page - * @param string $slug - * @param array $options - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to duplicate the page - */ - public static function duplicate(Page $page, string $slug, array $options = []): bool - { - if ($page->permissions()->duplicate() !== true) { - throw new PermissionException([ - 'key' => 'page.duplicate.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the page can be duplicated + * + * @param \Kirby\Cms\Page $page + * @param string $slug + * @param array $options + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to duplicate the page + */ + public static function duplicate(Page $page, string $slug, array $options = []): bool + { + if ($page->permissions()->duplicate() !== true) { + throw new PermissionException([ + 'key' => 'page.duplicate.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - self::validateSlugLength($slug); + self::validateSlugLength($slug); - return true; - } + return true; + } - /** - * Check if the page can be published - * (status change from draft to listed or unlisted) - * - * @param Page $page - * @return bool - */ - public static function publish(Page $page): bool - { - if ($page->permissions()->changeStatus() !== true) { - throw new PermissionException([ - 'key' => 'page.changeStatus.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Check if the page can be published + * (status change from draft to listed or unlisted) + * + * @param Page $page + * @return bool + */ + public static function publish(Page $page): bool + { + if ($page->permissions()->changeStatus() !== true) { + throw new PermissionException([ + 'key' => 'page.changeStatus.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - if ($page->isDraft() === true && empty($page->errors()) === false) { - throw new PermissionException([ - 'key' => 'page.changeStatus.incomplete', - 'details' => $page->errors() - ]); - } + if ($page->isDraft() === true && empty($page->errors()) === false) { + throw new PermissionException([ + 'key' => 'page.changeStatus.incomplete', + 'details' => $page->errors() + ]); + } - return true; - } + return true; + } - /** - * Validates if the page can be updated - * - * @param \Kirby\Cms\Page $page - * @param array $content - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the page - */ - public static function update(Page $page, array $content = []): bool - { - if ($page->permissions()->update() !== true) { - throw new PermissionException([ - 'key' => 'page.update.permission', - 'data' => [ - 'slug' => $page->slug() - ] - ]); - } + /** + * Validates if the page can be updated + * + * @param \Kirby\Cms\Page $page + * @param array $content + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the page + */ + public static function update(Page $page, array $content = []): bool + { + if ($page->permissions()->update() !== true) { + throw new PermissionException([ + 'key' => 'page.update.permission', + 'data' => [ + 'slug' => $page->slug() + ] + ]); + } - return true; - } + return true; + } - /** - * Ensures that the slug is not empty and doesn't exceed the maximum length - * to make sure that the directory name will be accepted by the filesystem - * - * @param string $slug New slug to check - * @return void - * @throws \Kirby\Exception\InvalidArgumentException If the slug is empty or too long - */ - protected static function validateSlugLength(string $slug): void - { - $slugLength = Str::length($slug); + /** + * Ensures that the slug is not empty and doesn't exceed the maximum length + * to make sure that the directory name will be accepted by the filesystem + * + * @param string $slug New slug to check + * @return void + * @throws \Kirby\Exception\InvalidArgumentException If the slug is empty or too long + */ + protected static function validateSlugLength(string $slug): void + { + $slugLength = Str::length($slug); - if ($slugLength === 0) { - throw new InvalidArgumentException([ - 'key' => 'page.slug.invalid', - ]); - } + if ($slugLength === 0) { + throw new InvalidArgumentException([ + 'key' => 'page.slug.invalid', + ]); + } - if ($slugsMaxlength = App::instance()->option('slugs.maxlength', 255)) { - $maxlength = (int)$slugsMaxlength; + if ($slugsMaxlength = App::instance()->option('slugs.maxlength', 255)) { + $maxlength = (int)$slugsMaxlength; - if ($slugLength > $maxlength) { - throw new InvalidArgumentException([ - 'key' => 'page.slug.maxlength', - 'data' => [ - 'length' => $maxlength - ] - ]); - } - } - } + if ($slugLength > $maxlength) { + throw new InvalidArgumentException([ + 'key' => 'page.slug.maxlength', + 'data' => [ + 'length' => $maxlength + ] + ]); + } + } + } } diff --git a/kirby/src/Cms/PageSiblings.php b/kirby/src/Cms/PageSiblings.php index 3b880c0..2507a1d 100644 --- a/kirby/src/Cms/PageSiblings.php +++ b/kirby/src/Cms/PageSiblings.php @@ -13,128 +13,128 @@ namespace Kirby\Cms; */ trait PageSiblings { - /** - * Checks if there's a next listed - * page in the siblings collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function hasNextListed($collection = null): bool - { - return $this->nextListed($collection) !== null; - } + /** + * Checks if there's a next listed + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasNextListed($collection = null): bool + { + return $this->nextListed($collection) !== null; + } - /** - * Checks if there's a next unlisted - * page in the siblings collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function hasNextUnlisted($collection = null): bool - { - return $this->nextUnlisted($collection) !== null; - } + /** + * Checks if there's a next unlisted + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasNextUnlisted($collection = null): bool + { + return $this->nextUnlisted($collection) !== null; + } - /** - * Checks if there's a previous listed - * page in the siblings collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function hasPrevListed($collection = null): bool - { - return $this->prevListed($collection) !== null; - } + /** + * Checks if there's a previous listed + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasPrevListed($collection = null): bool + { + return $this->prevListed($collection) !== null; + } - /** - * Checks if there's a previous unlisted - * page in the siblings collection - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return bool - */ - public function hasPrevUnlisted($collection = null): bool - { - return $this->prevUnlisted($collection) !== null; - } + /** + * Checks if there's a previous unlisted + * page in the siblings collection + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return bool + */ + public function hasPrevUnlisted($collection = null): bool + { + return $this->prevUnlisted($collection) !== null; + } - /** - * Returns the next listed page if it exists - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Page|null - */ - public function nextListed($collection = null) - { - return $this->nextAll($collection)->listed()->first(); - } + /** + * Returns the next listed page if it exists + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function nextListed($collection = null) + { + return $this->nextAll($collection)->listed()->first(); + } - /** - * Returns the next unlisted page if it exists - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Page|null - */ - public function nextUnlisted($collection = null) - { - return $this->nextAll($collection)->unlisted()->first(); - } + /** + * Returns the next unlisted page if it exists + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function nextUnlisted($collection = null) + { + return $this->nextAll($collection)->unlisted()->first(); + } - /** - * Returns the previous listed page - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Page|null - */ - public function prevListed($collection = null) - { - return $this->prevAll($collection)->listed()->last(); - } + /** + * Returns the previous listed page + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function prevListed($collection = null) + { + return $this->prevAll($collection)->listed()->last(); + } - /** - * Returns the previous unlisted page - * - * @param \Kirby\Cms\Collection|null $collection - * - * @return \Kirby\Cms\Page|null - */ - public function prevUnlisted($collection = null) - { - return $this->prevAll($collection)->unlisted()->last(); - } + /** + * Returns the previous unlisted page + * + * @param \Kirby\Cms\Collection|null $collection + * + * @return \Kirby\Cms\Page|null + */ + public function prevUnlisted($collection = null) + { + return $this->prevAll($collection)->unlisted()->last(); + } - /** - * Private siblings collector - * - * @return \Kirby\Cms\Collection - */ - protected function siblingsCollection() - { - if ($this->isDraft() === true) { - return $this->parentModel()->drafts(); - } else { - return $this->parentModel()->children(); - } - } + /** + * Private siblings collector + * + * @return \Kirby\Cms\Collection + */ + protected function siblingsCollection() + { + if ($this->isDraft() === true) { + return $this->parentModel()->drafts(); + } else { + return $this->parentModel()->children(); + } + } - /** - * Returns siblings with the same template - * - * @param bool $self - * @return \Kirby\Cms\Pages - */ - public function templateSiblings(bool $self = true) - { - return $this->siblings($self)->filter('intendedTemplate', $this->intendedTemplate()->name()); - } + /** + * Returns siblings with the same template + * + * @param bool $self + * @return \Kirby\Cms\Pages + */ + public function templateSiblings(bool $self = true) + { + return $this->siblings($self)->filter('intendedTemplate', $this->intendedTemplate()->name()); + } } diff --git a/kirby/src/Cms/Pages.php b/kirby/src/Cms/Pages.php index 2b9259a..5aa3959 100644 --- a/kirby/src/Cms/Pages.php +++ b/kirby/src/Cms/Pages.php @@ -22,499 +22,533 @@ use Kirby\Exception\InvalidArgumentException; */ class Pages extends Collection { - /** - * Cache for the index only listed and unlisted pages - * - * @var \Kirby\Cms\Pages|null - */ - protected $index = null; - - /** - * Cache for the index all statuses also including drafts - * - * @var \Kirby\Cms\Pages|null - */ - protected $indexWithDrafts = null; - - /** - * All registered pages methods - * - * @var array - */ - public static $methods = []; - - /** - * Adds a single page or - * an entire second collection to the - * current collection - * - * @param \Kirby\Cms\Pages|\Kirby\Cms\Page|string $object - * @return $this - * @throws \Kirby\Exception\InvalidArgumentException When no `Page` or `Pages` object or an ID of an existing page is passed - */ - public function add($object) - { - // add a pages collection - if (is_a($object, self::class) === true) { - $this->data = array_merge($this->data, $object->data); - - // add a page by id - } elseif (is_string($object) === true && $page = page($object)) { - $this->__set($page->id(), $page); - - // add a page object - } elseif (is_a($object, 'Kirby\Cms\Page') === true) { - $this->__set($object->id(), $object); - - // give a useful error message on invalid input; - // silently ignore "empty" values for compatibility with existing setups - } elseif (in_array($object, [null, false, true], true) !== true) { - throw new InvalidArgumentException('You must pass a Pages or Page object or an ID of an existing page to the Pages collection'); - } - - return $this; - } - - /** - * Returns all audio files of all children - * - * @return \Kirby\Cms\Files - */ - public function audio() - { - return $this->files()->filter('type', 'audio'); - } - - /** - * Returns all children for each page in the array - * - * @return \Kirby\Cms\Pages - */ - public function children() - { - $children = new Pages([], $this->parent); - - foreach ($this->data as $page) { - foreach ($page->children() as $childKey => $child) { - $children->data[$childKey] = $child; - } - } - - return $children; - } - - /** - * Returns all code files of all children - * - * @return \Kirby\Cms\Files - */ - public function code() - { - return $this->files()->filter('type', 'code'); - } - - /** - * Returns all documents of all children - * - * @return \Kirby\Cms\Files - */ - public function documents() - { - return $this->files()->filter('type', 'document'); - } - - /** - * Fetch all drafts for all pages in the collection - * - * @return \Kirby\Cms\Pages - */ - public function drafts() - { - $drafts = new Pages([], $this->parent); - - foreach ($this->data as $page) { - foreach ($page->drafts() as $draftKey => $draft) { - $drafts->data[$draftKey] = $draft; - } - } - - return $drafts; - } - - /** - * Creates a pages collection from an array of props - * - * @param array $pages - * @param \Kirby\Cms\Model|null $model - * @param bool $draft - * @return static - */ - public static function factory(array $pages, Model $model = null, bool $draft = false) - { - $model ??= App::instance()->site(); - $children = new static([], $model); - $kirby = $model->kirby(); - - if (is_a($model, 'Kirby\Cms\Page') === true) { - $parent = $model; - $site = $model->site(); - } else { - $parent = null; - $site = $model; - } - - foreach ($pages as $props) { - $props['kirby'] = $kirby; - $props['parent'] = $parent; - $props['site'] = $site; - $props['isDraft'] = $draft; - - $page = Page::factory($props); - - $children->data[$page->id()] = $page; - } - - return $children; - } - - /** - * Returns all files of all children - * - * @return \Kirby\Cms\Files - */ - public function files() - { - $files = new Files([], $this->parent); - - foreach ($this->data as $page) { - foreach ($page->files() as $fileKey => $file) { - $files->data[$fileKey] = $file; - } - } - - return $files; - } - - /** - * Finds a page in the collection by id. - * This works recursively for children and - * children of children, etc. - * - * @param string|null $id - * @return mixed - */ - public function findById(string $id = null) - { - if ($id === null) { - return null; - } - - // remove trailing or leading slashes - $id = trim($id, '/'); - - // strip extensions from the id - if (strpos($id, '.') !== false) { - $info = pathinfo($id); - - if ($info['dirname'] !== '.') { - $id = $info['dirname'] . '/' . $info['filename']; - } else { - $id = $info['filename']; - } - } - - // try the obvious way - if ($page = $this->get($id)) { - return $page; - } - - $start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : ''; - $page = $this->findByIdRecursive($id, $start, App::instance()->multilang()); - - return $page; - } - - /** - * Finds a child or child of a child recursively. - * - * @param string $id - * @param string|null $startAt - * @param bool $multiLang - * @return mixed - */ - public function findByIdRecursive(string $id, string $startAt = null, bool $multiLang = false) - { - $path = explode('/', $id); - $item = null; - $query = $startAt; - - foreach ($path as $key) { - $collection = $item ? $item->children() : $this; - $query = ltrim($query . '/' . $key, '/'); - $item = $collection->get($query) ?? null; - - if ($item === null && $multiLang === true && !App::instance()->language()->isDefault()) { - if (count($path) > 1 || $collection->parent()) { - // either the desired path is definitely not a slug, or collection is the children of another collection - $item = $collection->findBy('slug', $key); - } else { - // desired path _could_ be a slug or a "top level" uri - $item = $collection->findBy('uri', $key); - } - } - - if ($item === null) { - return null; - } - } - - return $item; - } - - /** - * Uses the specialized find by id method - * - * @param string|null $key - * @return mixed - */ - public function findByKey(string $key = null) - { - return $this->findById($key); - } - - /** - * Alias for Pages::findById - * - * @param string $id - * @return \Kirby\Cms\Page|null - */ - public function findByUri(string $id) - { - return $this->findById($id); - } - - /** - * Finds the currently open page - * - * @return \Kirby\Cms\Page|null - */ - public function findOpen() - { - return $this->findBy('isOpen', true); - } - - /** - * Custom getter that is able to find - * extension pages - * - * @param string $key - * @param mixed $default - * @return \Kirby\Cms\Page|null - */ - public function get($key, $default = null) - { - if ($key === null) { - return null; - } - - if ($item = parent::get($key)) { - return $item; - } - - return App::instance()->extension('pages', $key); - } - - /** - * Returns all images of all children - * - * @return \Kirby\Cms\Files - */ - public function images() - { - return $this->files()->filter('type', 'image'); - } - - /** - * Create a recursive flat index of all - * pages and subpages, etc. - * - * @param bool $drafts - * @return \Kirby\Cms\Pages - */ - public function index(bool $drafts = false) - { - // get object property by cache mode - $index = $drafts === true ? $this->indexWithDrafts : $this->index; - - if (is_a($index, 'Kirby\Cms\Pages') === true) { - return $index; - } - - $index = new Pages([], $this->parent); - - foreach ($this->data as $pageKey => $page) { - $index->data[$pageKey] = $page; - $pageIndex = $page->index($drafts); - - if ($pageIndex) { - foreach ($pageIndex as $childKey => $child) { - $index->data[$childKey] = $child; - } - } - } - - if ($drafts === true) { - return $this->indexWithDrafts = $index; - } - - return $this->index = $index; - } - - /** - * Returns all listed pages in the collection - * - * @return \Kirby\Cms\Pages - */ - public function listed() - { - return $this->filter('isListed', '==', true); - } - - /** - * Returns all unlisted pages in the collection - * - * @return \Kirby\Cms\Pages - */ - public function unlisted() - { - return $this->filter('isUnlisted', '==', true); - } - - /** - * Include all given items in the collection - * - * @param mixed ...$args - * @return $this|static - */ - public function merge(...$args) - { - // merge multiple arguments at once - if (count($args) > 1) { - $collection = clone $this; - foreach ($args as $arg) { - $collection = $collection->merge($arg); - } - return $collection; - } - - // merge all parent drafts - if ($args[0] === 'drafts') { - if ($parent = $this->parent()) { - return $this->merge($parent->drafts()); - } - - return $this; - } - - // merge an entire collection - if (is_a($args[0], self::class) === true) { - $collection = clone $this; - $collection->data = array_merge($collection->data, $args[0]->data); - return $collection; - } - - // append a single page - if (is_a($args[0], 'Kirby\Cms\Page') === true) { - $collection = clone $this; - return $collection->set($args[0]->id(), $args[0]); - } - - // merge an array - if (is_array($args[0]) === true) { - $collection = clone $this; - foreach ($args[0] as $arg) { - $collection = $collection->merge($arg); - } - return $collection; - } - - if (is_string($args[0]) === true) { - return $this->merge(App::instance()->site()->find($args[0])); - } - - return $this; - } - - /** - * Filter all pages by excluding the given template - * @since 3.3.0 - * - * @param string|array $templates - * @return \Kirby\Cms\Pages - */ - public function notTemplate($templates) - { - if (empty($templates) === true) { - return $this; - } - - if (is_array($templates) === false) { - $templates = [$templates]; - } - - return $this->filter(function ($page) use ($templates) { - return !in_array($page->intendedTemplate()->name(), $templates); - }); - } - - /** - * Returns an array with all page numbers - * - * @return array - */ - public function nums(): array - { - return $this->pluck('num'); - } - - /* - * Returns all listed and unlisted pages in the collection - * - * @return \Kirby\Cms\Pages - */ - public function published() - { - return $this->filter('isDraft', '==', false); - } - - /** - * Filter all pages by the given template - * - * @param string|array $templates - * @return \Kirby\Cms\Pages - */ - public function template($templates) - { - if (empty($templates) === true) { - return $this; - } - - if (is_array($templates) === false) { - $templates = [$templates]; - } - - return $this->filter(function ($page) use ($templates) { - return in_array($page->intendedTemplate()->name(), $templates); - }); - } - - /** - * Returns all video files of all children - * - * @return \Kirby\Cms\Files - */ - public function videos() - { - return $this->files()->filter('type', 'video'); - } + /** + * Cache for the index only listed and unlisted pages + * + * @var \Kirby\Cms\Pages|null + */ + protected $index = null; + + /** + * Cache for the index all statuses also including drafts + * + * @var \Kirby\Cms\Pages|null + */ + protected $indexWithDrafts = null; + + /** + * All registered pages methods + * + * @var array + */ + public static $methods = []; + + /** + * Adds a single page or + * an entire second collection to the + * current collection + * + * @param \Kirby\Cms\Pages|\Kirby\Cms\Page|string $object + * @return $this + * @throws \Kirby\Exception\InvalidArgumentException When no `Page` or `Pages` object or an ID of an existing page is passed + */ + public function add($object) + { + $site = App::instance()->site(); + + // add a pages collection + if (is_a($object, self::class) === true) { + $this->data = array_merge($this->data, $object->data); + + // add a page by id + } elseif (is_string($object) === true && $page = $site->find($object)) { + $this->__set($page->id(), $page); + + // add a page object + } elseif (is_a($object, 'Kirby\Cms\Page') === true) { + $this->__set($object->id(), $object); + + // give a useful error message on invalid input; + // silently ignore "empty" values for compatibility with existing setups + } elseif (in_array($object, [null, false, true], true) !== true) { + throw new InvalidArgumentException('You must pass a Pages or Page object or an ID of an existing page to the Pages collection'); + } + + return $this; + } + + /** + * Returns all audio files of all children + * + * @return \Kirby\Cms\Files + */ + public function audio() + { + return $this->files()->filter('type', 'audio'); + } + + /** + * Returns all children for each page in the array + * + * @return \Kirby\Cms\Pages + */ + public function children() + { + $children = new Pages([]); + + foreach ($this->data as $page) { + foreach ($page->children() as $childKey => $child) { + $children->data[$childKey] = $child; + } + } + + return $children; + } + + /** + * Returns all code files of all children + * + * @return \Kirby\Cms\Files + */ + public function code() + { + return $this->files()->filter('type', 'code'); + } + + /** + * Returns all documents of all children + * + * @return \Kirby\Cms\Files + */ + public function documents() + { + return $this->files()->filter('type', 'document'); + } + + /** + * Fetch all drafts for all pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function drafts() + { + $drafts = new Pages([]); + + foreach ($this->data as $page) { + foreach ($page->drafts() as $draftKey => $draft) { + $drafts->data[$draftKey] = $draft; + } + } + + return $drafts; + } + + /** + * Creates a pages collection from an array of props + * + * @param array $pages + * @param \Kirby\Cms\Model|null $model + * @param bool $draft + * @return static + */ + public static function factory(array $pages, Model $model = null, bool $draft = false) + { + $model ??= App::instance()->site(); + $children = new static([], $model); + $kirby = $model->kirby(); + + if (is_a($model, 'Kirby\Cms\Page') === true) { + $parent = $model; + $site = $model->site(); + } else { + $parent = null; + $site = $model; + } + + foreach ($pages as $props) { + $props['kirby'] = $kirby; + $props['parent'] = $parent; + $props['site'] = $site; + $props['isDraft'] = $draft; + + $page = Page::factory($props); + + $children->data[$page->id()] = $page; + } + + return $children; + } + + /** + * Returns all files of all children + * + * @return \Kirby\Cms\Files + */ + public function files() + { + $files = new Files([], $this->parent); + + foreach ($this->data as $page) { + foreach ($page->files() as $fileKey => $file) { + $files->data[$fileKey] = $file; + } + } + + return $files; + } + + /** + * Finds a page in the collection by id. + * This works recursively for children and + * children of children, etc. + * @deprecated 3.7.0 Use `$pages->get()` or `$pages->find()` instead + * @todo 3.8.0 Remove method + * @codeCoverageIgnore + * + * @param string|null $id + * @return mixed + */ + public function findById(string $id = null) + { + Helpers::deprecated('Cms\Pages::findById() has been deprecated and will be removed in Kirby 3.8.0. Use $pages->get() or $pages->find() instead.'); + + return $this->findByKey($id); + } + + /** + * Finds a child or child of a child recursively. + * @deprecated 3.7.0 Use `$pages->find()` instead + * @todo 3.8.0 Integrate code into `findByKey()` and remove this method + * + * @param string $id + * @param string|null $startAt + * @param bool $multiLang + * @return mixed + */ + public function findByIdRecursive(string $id, string $startAt = null, bool $multiLang = false, bool $silenceWarning = false) + { + // @codeCoverageIgnoreStart + if ($silenceWarning !== true) { + Helpers::deprecated('Cms\Pages::findByIdRecursive() has been deprecated and will be removed in Kirby 3.8.0. Use $pages->find() instead.'); + } + // @codeCoverageIgnoreEnd + + $path = explode('/', $id); + $item = null; + $query = $startAt; + + foreach ($path as $key) { + $collection = $item ? $item->children() : $this; + $query = ltrim($query . '/' . $key, '/'); + $item = $collection->get($query) ?? null; + + if ($item === null && $multiLang === true && !App::instance()->language()->isDefault()) { + if (count($path) > 1 || $collection->parent()) { + // either the desired path is definitely not a slug, or collection is the children of another collection + $item = $collection->findBy('slug', $key); + } else { + // desired path _could_ be a slug or a "top level" uri + $item = $collection->findBy('uri', $key); + } + } + + if ($item === null) { + return null; + } + } + + return $item; + } + + /** + * Finds a page by its ID or URI + * @internal Use `$pages->find()` instead + * + * @param string|null $key + * @return \Kirby\Cms\Page|null + */ + public function findByKey(?string $key = null) + { + if ($key === null) { + return null; + } + + // remove trailing or leading slashes + $key = trim($key, '/'); + + // strip extensions from the id + if (strpos($key, '.') !== false) { + $info = pathinfo($key); + + if ($info['dirname'] !== '.') { + $key = $info['dirname'] . '/' . $info['filename']; + } else { + $key = $info['filename']; + } + } + + // try the obvious way + if ($page = $this->get($key)) { + return $page; + } + + // try to find the page by its (translated) URI by stepping through the page tree + $start = is_a($this->parent, 'Kirby\Cms\Page') === true ? $this->parent->id() : ''; + if ($page = $this->findByIdRecursive($key, $start, App::instance()->multilang(), true)) { + return $page; + } + + // for secondary languages, try the full translated URI + // (for collections without parent that won't have a result above) + if ( + App::instance()->multilang() === true && + App::instance()->language()->isDefault() === false && + $page = $this->findBy('uri', $key) + ) { + return $page; + } + + return null; + } + + /** + * Alias for `$pages->find()` + * @deprecated 3.7.0 Use `$pages->find()` instead + * @todo 3.8.0 Remove method + * @codeCoverageIgnore + * + * @param string $id + * @return \Kirby\Cms\Page|null + */ + public function findByUri(string $id) + { + Helpers::deprecated('Cms\Pages::findByUri() has been deprecated and will be removed in Kirby 3.8.0. Use $pages->find() instead.'); + + return $this->findByKey($id); + } + + /** + * Finds the currently open page + * + * @return \Kirby\Cms\Page|null + */ + public function findOpen() + { + return $this->findBy('isOpen', true); + } + + /** + * Custom getter that is able to find + * extension pages + * + * @param string $key + * @param mixed $default + * @return \Kirby\Cms\Page|null + */ + public function get($key, $default = null) + { + if ($key === null) { + return null; + } + + if ($item = parent::get($key)) { + return $item; + } + + return App::instance()->extension('pages', $key); + } + + /** + * Returns all images of all children + * + * @return \Kirby\Cms\Files + */ + public function images() + { + return $this->files()->filter('type', 'image'); + } + + /** + * Create a recursive flat index of all + * pages and subpages, etc. + * + * @param bool $drafts + * @return \Kirby\Cms\Pages + */ + public function index(bool $drafts = false) + { + // get object property by cache mode + $index = $drafts === true ? $this->indexWithDrafts : $this->index; + + if (is_a($index, 'Kirby\Cms\Pages') === true) { + return $index; + } + + $index = new Pages([]); + + foreach ($this->data as $pageKey => $page) { + $index->data[$pageKey] = $page; + $pageIndex = $page->index($drafts); + + if ($pageIndex) { + foreach ($pageIndex as $childKey => $child) { + $index->data[$childKey] = $child; + } + } + } + + if ($drafts === true) { + return $this->indexWithDrafts = $index; + } + + return $this->index = $index; + } + + /** + * Returns all listed pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function listed() + { + return $this->filter('isListed', '==', true); + } + + /** + * Returns all unlisted pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function unlisted() + { + return $this->filter('isUnlisted', '==', true); + } + + /** + * Include all given items in the collection + * + * @param mixed ...$args + * @return $this|static + */ + public function merge(...$args) + { + // merge multiple arguments at once + if (count($args) > 1) { + $collection = clone $this; + foreach ($args as $arg) { + $collection = $collection->merge($arg); + } + return $collection; + } + + // merge all parent drafts + if ($args[0] === 'drafts') { + if ($parent = $this->parent()) { + return $this->merge($parent->drafts()); + } + + return $this; + } + + // merge an entire collection + if (is_a($args[0], self::class) === true) { + $collection = clone $this; + $collection->data = array_merge($collection->data, $args[0]->data); + return $collection; + } + + // append a single page + if (is_a($args[0], 'Kirby\Cms\Page') === true) { + $collection = clone $this; + return $collection->set($args[0]->id(), $args[0]); + } + + // merge an array + if (is_array($args[0]) === true) { + $collection = clone $this; + foreach ($args[0] as $arg) { + $collection = $collection->merge($arg); + } + return $collection; + } + + if (is_string($args[0]) === true) { + return $this->merge(App::instance()->site()->find($args[0])); + } + + return $this; + } + + /** + * Filter all pages by excluding the given template + * @since 3.3.0 + * + * @param string|array $templates + * @return \Kirby\Cms\Pages + */ + public function notTemplate($templates) + { + if (empty($templates) === true) { + return $this; + } + + if (is_array($templates) === false) { + $templates = [$templates]; + } + + return $this->filter(function ($page) use ($templates) { + return !in_array($page->intendedTemplate()->name(), $templates); + }); + } + + /** + * Returns an array with all page numbers + * + * @return array + */ + public function nums(): array + { + return $this->pluck('num'); + } + + /* + * Returns all listed and unlisted pages in the collection + * + * @return \Kirby\Cms\Pages + */ + public function published() + { + return $this->filter('isDraft', '==', false); + } + + /** + * Filter all pages by the given template + * + * @param string|array $templates + * @return \Kirby\Cms\Pages + */ + public function template($templates) + { + if (empty($templates) === true) { + return $this; + } + + if (is_array($templates) === false) { + $templates = [$templates]; + } + + return $this->filter(function ($page) use ($templates) { + return in_array($page->intendedTemplate()->name(), $templates); + }); + } + + /** + * Returns all video files of all children + * + * @return \Kirby\Cms\Files + */ + public function videos() + { + return $this->files()->filter('type', 'video'); + } } diff --git a/kirby/src/Cms/Pagination.php b/kirby/src/Cms/Pagination.php index ec65276..13f6ef2 100644 --- a/kirby/src/Cms/Pagination.php +++ b/kirby/src/Cms/Pagination.php @@ -22,158 +22,158 @@ use Kirby\Toolkit\Pagination as BasePagination; */ class Pagination extends BasePagination { - /** - * Pagination method (param, query, none) - * - * @var string - */ - protected $method; + /** + * Pagination method (param, query, none) + * + * @var string + */ + protected $method; - /** - * The base URL - * - * @var string - */ - protected $url; + /** + * The base URL + * + * @var string + */ + protected $url; - /** - * Variable name for query strings - * - * @var string - */ - protected $variable; + /** + * Variable name for query strings + * + * @var string + */ + protected $variable; - /** - * Creates the pagination object. As a new - * property you can now pass the base Url. - * That Url must be the Url of the first - * page of the collection without additional - * pagination information/query parameters in it. - * - * ```php - * $pagination = new Pagination([ - * 'page' => 1, - * 'limit' => 10, - * 'total' => 120, - * 'method' => 'query', - * 'variable' => 'p', - * 'url' => new Uri('https://getkirby.com/blog') - * ]); - * ``` - * - * @param array $params - */ - public function __construct(array $params = []) - { - $kirby = App::instance(); - $config = $kirby->option('pagination', []); - $request = $kirby->request(); + /** + * Creates the pagination object. As a new + * property you can now pass the base Url. + * That Url must be the Url of the first + * page of the collection without additional + * pagination information/query parameters in it. + * + * ```php + * $pagination = new Pagination([ + * 'page' => 1, + * 'limit' => 10, + * 'total' => 120, + * 'method' => 'query', + * 'variable' => 'p', + * 'url' => new Uri('https://getkirby.com/blog') + * ]); + * ``` + * + * @param array $params + */ + public function __construct(array $params = []) + { + $kirby = App::instance(); + $config = $kirby->option('pagination', []); + $request = $kirby->request(); - $params['limit'] ??= $config['limit'] ?? 20; - $params['method'] ??= $config['method'] ?? 'param'; - $params['variable'] ??= $config['variable'] ?? 'page'; + $params['limit'] ??= $config['limit'] ?? 20; + $params['method'] ??= $config['method'] ?? 'param'; + $params['variable'] ??= $config['variable'] ?? 'page'; - if (empty($params['url']) === true) { - $params['url'] = new Uri($kirby->url('current'), [ - 'params' => $request->params(), - 'query' => $request->query()->toArray(), - ]); - } + if (empty($params['url']) === true) { + $params['url'] = new Uri($kirby->url('current'), [ + 'params' => $request->params(), + 'query' => $request->query()->toArray(), + ]); + } - if ($params['method'] === 'query') { - $params['page'] ??= $params['url']->query()->get($params['variable']); - } elseif ($params['method'] === 'param') { - $params['page'] ??= $params['url']->params()->get($params['variable']); - } + if ($params['method'] === 'query') { + $params['page'] ??= $params['url']->query()->get($params['variable']); + } elseif ($params['method'] === 'param') { + $params['page'] ??= $params['url']->params()->get($params['variable']); + } - parent::__construct($params); + parent::__construct($params); - $this->method = $params['method']; - $this->url = $params['url']; - $this->variable = $params['variable']; - } + $this->method = $params['method']; + $this->url = $params['url']; + $this->variable = $params['variable']; + } - /** - * Returns the Url for the first page - * - * @return string - */ - public function firstPageUrl(): string - { - return $this->pageUrl(1); - } + /** + * Returns the Url for the first page + * + * @return string|null + */ + public function firstPageUrl(): ?string + { + return $this->pageUrl(1); + } - /** - * Returns the Url for the last page - * - * @return string - */ - public function lastPageUrl(): string - { - return $this->pageUrl($this->lastPage()); - } + /** + * Returns the Url for the last page + * + * @return string|null + */ + public function lastPageUrl(): ?string + { + return $this->pageUrl($this->lastPage()); + } - /** - * Returns the Url for the next page. - * Returns null if there's no next page. - * - * @return string|null - */ - public function nextPageUrl(): ?string - { - if ($page = $this->nextPage()) { - return $this->pageUrl($page); - } + /** + * Returns the Url for the next page. + * Returns null if there's no next page. + * + * @return string|null + */ + public function nextPageUrl(): ?string + { + if ($page = $this->nextPage()) { + return $this->pageUrl($page); + } - return null; - } + return null; + } - /** - * Returns the URL of the current page. - * If the `$page` variable is set, the URL - * for that page will be returned. - * - * @param int|null $page - * @return string|null - */ - public function pageUrl(int $page = null): ?string - { - if ($page === null) { - return $this->pageUrl($this->page()); - } + /** + * Returns the URL of the current page. + * If the `$page` variable is set, the URL + * for that page will be returned. + * + * @param int|null $page + * @return string|null + */ + public function pageUrl(int $page = null): ?string + { + if ($page === null) { + return $this->pageUrl($this->page()); + } - $url = clone $this->url; - $variable = $this->variable; + $url = clone $this->url; + $variable = $this->variable; - if ($this->hasPage($page) === false) { - return null; - } + if ($this->hasPage($page) === false) { + return null; + } - $pageValue = $page === 1 ? null : $page; + $pageValue = $page === 1 ? null : $page; - if ($this->method === 'query') { - $url->query->$variable = $pageValue; - } elseif ($this->method === 'param') { - $url->params->$variable = $pageValue; - } else { - return null; - } + if ($this->method === 'query') { + $url->query->$variable = $pageValue; + } elseif ($this->method === 'param') { + $url->params->$variable = $pageValue; + } else { + return null; + } - return $url->toString(); - } + return $url->toString(); + } - /** - * Returns the Url for the previous page. - * Returns null if there's no previous page. - * - * @return string|null - */ - public function prevPageUrl(): ?string - { - if ($page = $this->prevPage()) { - return $this->pageUrl($page); - } + /** + * Returns the Url for the previous page. + * Returns null if there's no previous page. + * + * @return string|null + */ + public function prevPageUrl(): ?string + { + if ($page = $this->prevPage()) { + return $this->pageUrl($page); + } - return null; - } + return null; + } } diff --git a/kirby/src/Cms/Permissions.php b/kirby/src/Cms/Permissions.php index 4a01379..caaa284 100644 --- a/kirby/src/Cms/Permissions.php +++ b/kirby/src/Cms/Permissions.php @@ -17,222 +17,222 @@ use Kirby\Exception\InvalidArgumentException; */ class Permissions { - /** - * @var array - */ - public static $extendedActions = []; + /** + * @var array + */ + public static $extendedActions = []; - /** - * @var array - */ - protected $actions = [ - 'access' => [ - 'account' => true, - 'languages' => true, - 'panel' => true, - 'site' => true, - 'system' => true, - 'users' => true, - ], - 'files' => [ - 'changeName' => true, - 'create' => true, - 'delete' => true, - 'read' => true, - 'replace' => true, - 'update' => true - ], - 'languages' => [ - 'create' => true, - 'delete' => true - ], - 'pages' => [ - 'changeSlug' => true, - 'changeStatus' => true, - 'changeTemplate' => true, - 'changeTitle' => true, - 'create' => true, - 'delete' => true, - 'duplicate' => true, - 'preview' => true, - 'read' => true, - 'sort' => true, - 'update' => true - ], - 'site' => [ - 'changeTitle' => true, - 'update' => true - ], - 'users' => [ - 'changeEmail' => true, - 'changeLanguage' => true, - 'changeName' => true, - 'changePassword' => true, - 'changeRole' => true, - 'create' => true, - 'delete' => true, - 'update' => true - ], - 'user' => [ - 'changeEmail' => true, - 'changeLanguage' => true, - 'changeName' => true, - 'changePassword' => true, - 'changeRole' => true, - 'delete' => true, - 'update' => true - ] - ]; + /** + * @var array + */ + protected $actions = [ + 'access' => [ + 'account' => true, + 'languages' => true, + 'panel' => true, + 'site' => true, + 'system' => true, + 'users' => true, + ], + 'files' => [ + 'changeName' => true, + 'create' => true, + 'delete' => true, + 'read' => true, + 'replace' => true, + 'update' => true + ], + 'languages' => [ + 'create' => true, + 'delete' => true + ], + 'pages' => [ + 'changeSlug' => true, + 'changeStatus' => true, + 'changeTemplate' => true, + 'changeTitle' => true, + 'create' => true, + 'delete' => true, + 'duplicate' => true, + 'preview' => true, + 'read' => true, + 'sort' => true, + 'update' => true + ], + 'site' => [ + 'changeTitle' => true, + 'update' => true + ], + 'users' => [ + 'changeEmail' => true, + 'changeLanguage' => true, + 'changeName' => true, + 'changePassword' => true, + 'changeRole' => true, + 'create' => true, + 'delete' => true, + 'update' => true + ], + 'user' => [ + 'changeEmail' => true, + 'changeLanguage' => true, + 'changeName' => true, + 'changePassword' => true, + 'changeRole' => true, + 'delete' => true, + 'update' => true + ] + ]; - /** - * Permissions constructor - * - * @param array $settings - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function __construct($settings = []) - { - // dynamically register the extended actions - foreach (static::$extendedActions as $key => $actions) { - if (isset($this->actions[$key]) === true) { - throw new InvalidArgumentException('The action ' . $key . ' is already a core action'); - } + /** + * Permissions constructor + * + * @param array $settings + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct($settings = []) + { + // dynamically register the extended actions + foreach (static::$extendedActions as $key => $actions) { + if (isset($this->actions[$key]) === true) { + throw new InvalidArgumentException('The action ' . $key . ' is already a core action'); + } - $this->actions[$key] = $actions; - } + $this->actions[$key] = $actions; + } - if (is_array($settings) === true) { - return $this->setCategories($settings); - } + if (is_array($settings) === true) { + return $this->setCategories($settings); + } - if (is_bool($settings) === true) { - return $this->setAll($settings); - } - } + if (is_bool($settings) === true) { + return $this->setAll($settings); + } + } - /** - * @param string|null $category - * @param string|null $action - * @return bool - */ - public function for(string $category = null, string $action = null): bool - { - if ($action === null) { - if ($this->hasCategory($category) === false) { - return false; - } + /** + * @param string|null $category + * @param string|null $action + * @return bool + */ + public function for(string $category = null, string $action = null): bool + { + if ($action === null) { + if ($this->hasCategory($category) === false) { + return false; + } - return $this->actions[$category]; - } + return $this->actions[$category]; + } - if ($this->hasAction($category, $action) === false) { - return false; - } + if ($this->hasAction($category, $action) === false) { + return false; + } - return $this->actions[$category][$action]; - } + return $this->actions[$category][$action]; + } - /** - * @param string $category - * @param string $action - * @return bool - */ - protected function hasAction(string $category, string $action): bool - { - return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true; - } + /** + * @param string $category + * @param string $action + * @return bool + */ + protected function hasAction(string $category, string $action): bool + { + return $this->hasCategory($category) === true && array_key_exists($action, $this->actions[$category]) === true; + } - /** - * @param string $category - * @return bool - */ - protected function hasCategory(string $category): bool - { - return array_key_exists($category, $this->actions) === true; - } + /** + * @param string $category + * @return bool + */ + protected function hasCategory(string $category): bool + { + return array_key_exists($category, $this->actions) === true; + } - /** - * @param string $category - * @param string $action - * @param $setting - * @return $this - */ - protected function setAction(string $category, string $action, $setting) - { - // deprecated fallback for the settings/system view - // TODO: remove in 3.7 - if ($category === 'access' && $action === 'settings') { - $action = 'system'; - } + /** + * @param string $category + * @param string $action + * @param $setting + * @return $this + */ + protected function setAction(string $category, string $action, $setting) + { + // deprecated fallback for the settings/system view + // TODO: remove in 3.8.0 + if ($category === 'access' && $action === 'settings') { + $action = 'system'; + } - // wildcard to overwrite the entire category - if ($action === '*') { - return $this->setCategory($category, $setting); - } + // wildcard to overwrite the entire category + if ($action === '*') { + return $this->setCategory($category, $setting); + } - $this->actions[$category][$action] = $setting; + $this->actions[$category][$action] = $setting; - return $this; - } + return $this; + } - /** - * @param bool $setting - * @return $this - */ - protected function setAll(bool $setting) - { - foreach ($this->actions as $categoryName => $actions) { - $this->setCategory($categoryName, $setting); - } + /** + * @param bool $setting + * @return $this + */ + protected function setAll(bool $setting) + { + foreach ($this->actions as $categoryName => $actions) { + $this->setCategory($categoryName, $setting); + } - return $this; - } + return $this; + } - /** - * @param array $settings - * @return $this - */ - protected function setCategories(array $settings) - { - foreach ($settings as $categoryName => $categoryActions) { - if (is_bool($categoryActions) === true) { - $this->setCategory($categoryName, $categoryActions); - } + /** + * @param array $settings + * @return $this + */ + protected function setCategories(array $settings) + { + foreach ($settings as $categoryName => $categoryActions) { + if (is_bool($categoryActions) === true) { + $this->setCategory($categoryName, $categoryActions); + } - if (is_array($categoryActions) === true) { - foreach ($categoryActions as $actionName => $actionSetting) { - $this->setAction($categoryName, $actionName, $actionSetting); - } - } - } + if (is_array($categoryActions) === true) { + foreach ($categoryActions as $actionName => $actionSetting) { + $this->setAction($categoryName, $actionName, $actionSetting); + } + } + } - return $this; - } + return $this; + } - /** - * @param string $category - * @param bool $setting - * @return $this - * @throws \Kirby\Exception\InvalidArgumentException - */ - protected function setCategory(string $category, bool $setting) - { - if ($this->hasCategory($category) === false) { - throw new InvalidArgumentException('Invalid permissions category'); - } + /** + * @param string $category + * @param bool $setting + * @return $this + * @throws \Kirby\Exception\InvalidArgumentException + */ + protected function setCategory(string $category, bool $setting) + { + if ($this->hasCategory($category) === false) { + throw new InvalidArgumentException('Invalid permissions category'); + } - foreach ($this->actions[$category] as $actionName => $actionSetting) { - $this->actions[$category][$actionName] = $setting; - } + foreach ($this->actions[$category] as $actionName => $actionSetting) { + $this->actions[$category][$actionName] = $setting; + } - return $this; - } + return $this; + } - /** - * @return array - */ - public function toArray(): array - { - return $this->actions; - } + /** + * @return array + */ + public function toArray(): array + { + return $this->actions; + } } diff --git a/kirby/src/Cms/Picker.php b/kirby/src/Cms/Picker.php index 9575359..7c26ad6 100644 --- a/kirby/src/Cms/Picker.php +++ b/kirby/src/Cms/Picker.php @@ -14,166 +14,166 @@ namespace Kirby\Cms; */ abstract class Picker { - /** - * @var \Kirby\Cms\App - */ - protected $kirby; + /** + * @var \Kirby\Cms\App + */ + protected $kirby; - /** - * @var array - */ - protected $options; + /** + * @var array + */ + protected $options; - /** - * @var \Kirby\Cms\Site - */ - protected $site; + /** + * @var \Kirby\Cms\Site + */ + protected $site; - /** - * Creates a new Picker instance - * - * @param array $params - */ - public function __construct(array $params = []) - { - $this->options = array_merge($this->defaults(), $params); - $this->kirby = $this->options['model']->kirby(); - $this->site = $this->kirby->site(); - } + /** + * Creates a new Picker instance + * + * @param array $params + */ + public function __construct(array $params = []) + { + $this->options = array_merge($this->defaults(), $params); + $this->kirby = $this->options['model']->kirby(); + $this->site = $this->kirby->site(); + } - /** - * Return the array of default values - * - * @return array - */ - protected function defaults(): array - { - // default params - return [ - // image settings (ratio, cover, etc.) - 'image' => [], - // query template for the info field - 'info' => false, - // listing style: list, cards, cardlets - 'layout' =>'list', - // number of users displayed per pagination page - 'limit' => 20, - // optional mapping function for the result array - 'map' => null, - // the reference model - 'model' => site(), - // current page when paginating - 'page' => 1, - // a query string to fetch specific items - 'query' => null, - // search query - 'search' => null, - // query template for the text field - 'text' => null - ]; - } + /** + * Return the array of default values + * + * @return array + */ + protected function defaults(): array + { + // default params + return [ + // image settings (ratio, cover, etc.) + 'image' => [], + // query template for the info field + 'info' => false, + // listing style: list, cards, cardlets + 'layout' =>'list', + // number of users displayed per pagination page + 'limit' => 20, + // optional mapping function for the result array + 'map' => null, + // the reference model + 'model' => App::instance()->site(), + // current page when paginating + 'page' => 1, + // a query string to fetch specific items + 'query' => null, + // search query + 'search' => null, + // query template for the text field + 'text' => null + ]; + } - /** - * Fetches all items for the picker - * - * @return \Kirby\Cms\Collection|null - */ - abstract public function items(); + /** + * Fetches all items for the picker + * + * @return \Kirby\Cms\Collection|null + */ + abstract public function items(); - /** - * Converts all given items to an associative - * array that is already optimized for the - * panel picker component. - * - * @param \Kirby\Cms\Collection|null $items - * @return array - */ - public function itemsToArray($items = null): array - { - if ($items === null) { - return []; - } + /** + * Converts all given items to an associative + * array that is already optimized for the + * panel picker component. + * + * @param \Kirby\Cms\Collection|null $items + * @return array + */ + public function itemsToArray($items = null): array + { + if ($items === null) { + return []; + } - $result = []; + $result = []; - foreach ($items as $index => $item) { - if (empty($this->options['map']) === false) { - $result[] = $this->options['map']($item); - } else { - $result[] = $item->panel()->pickerData([ - 'image' => $this->options['image'], - 'info' => $this->options['info'], - 'layout' => $this->options['layout'], - 'model' => $this->options['model'], - 'text' => $this->options['text'], - ]); - } - } + foreach ($items as $index => $item) { + if (empty($this->options['map']) === false) { + $result[] = $this->options['map']($item); + } else { + $result[] = $item->panel()->pickerData([ + 'image' => $this->options['image'], + 'info' => $this->options['info'], + 'layout' => $this->options['layout'], + 'model' => $this->options['model'], + 'text' => $this->options['text'], + ]); + } + } - return $result; - } + return $result; + } - /** - * Apply pagination to the collection - * of items according to the options. - * - * @param \Kirby\Cms\Collection $items - * @return \Kirby\Cms\Collection - */ - public function paginate(Collection $items) - { - return $items->paginate([ - 'limit' => $this->options['limit'], - 'page' => $this->options['page'] - ]); - } + /** + * Apply pagination to the collection + * of items according to the options. + * + * @param \Kirby\Cms\Collection $items + * @return \Kirby\Cms\Collection + */ + public function paginate(Collection $items) + { + return $items->paginate([ + 'limit' => $this->options['limit'], + 'page' => $this->options['page'] + ]); + } - /** - * Return the most relevant pagination - * info as array - * - * @param \Kirby\Cms\Pagination $pagination - * @return array - */ - public function paginationToArray(Pagination $pagination): array - { - return [ - 'limit' => $pagination->limit(), - 'page' => $pagination->page(), - 'total' => $pagination->total() - ]; - } + /** + * Return the most relevant pagination + * info as array + * + * @param \Kirby\Cms\Pagination $pagination + * @return array + */ + public function paginationToArray(Pagination $pagination): array + { + return [ + 'limit' => $pagination->limit(), + 'page' => $pagination->page(), + 'total' => $pagination->total() + ]; + } - /** - * Search through the collection of items - * if not deactivate in the options - * - * @param \Kirby\Cms\Collection $items - * @return \Kirby\Cms\Collection - */ - public function search(Collection $items) - { - if (empty($this->options['search']) === false) { - return $items->search($this->options['search']); - } + /** + * Search through the collection of items + * if not deactivate in the options + * + * @param \Kirby\Cms\Collection $items + * @return \Kirby\Cms\Collection + */ + public function search(Collection $items) + { + if (empty($this->options['search']) === false) { + return $items->search($this->options['search']); + } - return $items; - } + return $items; + } - /** - * Returns an associative array - * with all information for the picker. - * This will be passed directly to the API. - * - * @return array - */ - public function toArray(): array - { - $items = $this->items(); + /** + * Returns an associative array + * with all information for the picker. + * This will be passed directly to the API. + * + * @return array + */ + public function toArray(): array + { + $items = $this->items(); - return [ - 'data' => $this->itemsToArray($items), - 'pagination' => $this->paginationToArray($items->pagination()), - ]; - } + return [ + 'data' => $this->itemsToArray($items), + 'pagination' => $this->paginationToArray($items->pagination()), + ]; + } } diff --git a/kirby/src/Cms/Plugin.php b/kirby/src/Cms/Plugin.php index c75138e..ae5e07f 100644 --- a/kirby/src/Cms/Plugin.php +++ b/kirby/src/Cms/Plugin.php @@ -20,202 +20,203 @@ use Kirby\Toolkit\V; */ class Plugin extends Model { - protected $extends; - protected $info; - protected $name; - protected $root; + protected $extends; + protected $info; + protected $name; + protected $root; - /** - * @param string $key - * @param array|null $arguments - * @return mixed|null - */ - public function __call(string $key, array $arguments = null) - { - return $this->info()[$key] ?? null; - } + /** + * @param string $key + * @param array|null $arguments + * @return mixed|null + */ + public function __call(string $key, array $arguments = null) + { + return $this->info()[$key] ?? null; + } - /** - * Plugin constructor - * - * @param string $name - * @param array $extends - */ - public function __construct(string $name, array $extends = []) - { - $this->setName($name); - $this->extends = $extends; - $this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); - $this->info = empty($extends['info']) === false && is_array($extends['info']) ? $extends['info'] : null; + /** + * Plugin constructor + * + * @param string $name + * @param array $extends + */ + public function __construct(string $name, array $extends = []) + { + $this->setName($name); + $this->extends = $extends; + $this->root = $extends['root'] ?? dirname(debug_backtrace()[0]['file']); + $this->info = empty($extends['info']) === false && is_array($extends['info']) ? $extends['info'] : null; - unset($this->extends['root'], $this->extends['info']); - } + unset($this->extends['root'], $this->extends['info']); + } - /** - * Returns the array with author information - * from the composer file - * - * @return array - */ - public function authors(): array - { - return $this->info()['authors'] ?? []; - } + /** + * Returns the array with author information + * from the composer file + * + * @return array + */ + public function authors(): array + { + return $this->info()['authors'] ?? []; + } - /** - * Returns a comma-separated list with all author names - * - * @return string - */ - public function authorsNames(): string - { - $names = []; + /** + * Returns a comma-separated list with all author names + * + * @return string + */ + public function authorsNames(): string + { + $names = []; - foreach ($this->authors() as $author) { - $names[] = $author['name'] ?? null; - } + foreach ($this->authors() as $author) { + $names[] = $author['name'] ?? null; + } - return implode(', ', array_filter($names)); - } + return implode(', ', array_filter($names)); + } - /** - * @return array - */ - public function extends(): array - { - return $this->extends; - } + /** + * @return array + */ + public function extends(): array + { + return $this->extends; + } - /** - * Returns the unique id for the plugin - * - * @return string - */ - public function id(): string - { - return $this->name(); - } + /** + * Returns the unique id for the plugin + * + * @return string + */ + public function id(): string + { + return $this->name(); + } - /** - * @return array - */ - public function info(): array - { - if (is_array($this->info) === true) { - return $this->info; - } + /** + * @return array + */ + public function info(): array + { + if (is_array($this->info) === true) { + return $this->info; + } - try { - $info = Data::read($this->manifest()); - } catch (Exception $e) { - // there is no manifest file or it is invalid - $info = []; - } + try { + $info = Data::read($this->manifest()); + } catch (Exception $e) { + // there is no manifest file or it is invalid + $info = []; + } - return $this->info = $info; - } + return $this->info = $info; + } - /** - * Returns the link to the plugin homepage - * - * @return string|null - */ - public function link(): ?string - { - $homepage = $this->info['homepage'] ?? null; - $docs = $this->info['support']['docs'] ?? null; - $source = $this->info['support']['source'] ?? null; + /** + * Returns the link to the plugin homepage + * + * @return string|null + */ + public function link(): ?string + { + $info = $this->info(); + $homepage = $info['homepage'] ?? null; + $docs = $info['support']['docs'] ?? null; + $source = $info['support']['source'] ?? null; - $link = $homepage ?? $docs ?? $source; + $link = $homepage ?? $docs ?? $source; - return V::url($link) ? $link : null; - } + return V::url($link) ? $link : null; + } - /** - * @return string - */ - public function manifest(): string - { - return $this->root() . '/composer.json'; - } + /** + * @return string + */ + public function manifest(): string + { + return $this->root() . '/composer.json'; + } - /** - * @return string - */ - public function mediaRoot(): string - { - return App::instance()->root('media') . '/plugins/' . $this->name(); - } + /** + * @return string + */ + public function mediaRoot(): string + { + return App::instance()->root('media') . '/plugins/' . $this->name(); + } - /** - * @return string - */ - public function mediaUrl(): string - { - return App::instance()->url('media') . '/plugins/' . $this->name(); - } + /** + * @return string + */ + public function mediaUrl(): string + { + return App::instance()->url('media') . '/plugins/' . $this->name(); + } - /** - * @return string - */ - public function name(): string - { - return $this->name; - } + /** + * @return string + */ + public function name(): string + { + return $this->name; + } - /** - * @param string $key - * @return mixed - */ - public function option(string $key) - { - return $this->kirby()->option($this->prefix() . '.' . $key); - } + /** + * @param string $key + * @return mixed + */ + public function option(string $key) + { + return $this->kirby()->option($this->prefix() . '.' . $key); + } - /** - * @return string - */ - public function prefix(): string - { - return str_replace('/', '.', $this->name()); - } + /** + * @return string + */ + public function prefix(): string + { + return str_replace('/', '.', $this->name()); + } - /** - * @return string - */ - public function root(): string - { - return $this->root; - } + /** + * @return string + */ + public function root(): string + { + return $this->root; + } - /** - * @param string $name - * @return $this - * @throws \Kirby\Exception\InvalidArgumentException - */ - protected function setName(string $name) - { - if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) !== 1) { - throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"'); - } + /** + * @param string $name + * @return $this + * @throws \Kirby\Exception\InvalidArgumentException + */ + protected function setName(string $name) + { + if (preg_match('!^[a-z0-9-]+\/[a-z0-9-]+$!i', $name) !== 1) { + throw new InvalidArgumentException('The plugin name must follow the format "a-z0-9-/a-z0-9-"'); + } - $this->name = $name; - return $this; - } + $this->name = $name; + return $this; + } - /** - * @return array - */ - public function toArray(): array - { - return [ - 'authors' => $this->authors(), - 'description' => $this->description(), - 'name' => $this->name(), - 'license' => $this->license(), - 'link' => $this->link(), - 'root' => $this->root(), - 'version' => $this->version() - ]; - } + /** + * @return array + */ + public function toArray(): array + { + return [ + 'authors' => $this->authors(), + 'description' => $this->description(), + 'name' => $this->name(), + 'license' => $this->license(), + 'link' => $this->link(), + 'root' => $this->root(), + 'version' => $this->version() + ]; + } } diff --git a/kirby/src/Cms/PluginAssets.php b/kirby/src/Cms/PluginAssets.php index 6af559a..a05ffea 100644 --- a/kirby/src/Cms/PluginAssets.php +++ b/kirby/src/Cms/PluginAssets.php @@ -19,62 +19,62 @@ use Kirby\Http\Response; */ class PluginAssets { - /** - * Clean old/deprecated assets on every resolve - * - * @param string $pluginName - * @return void - */ - public static function clean(string $pluginName): void - { - if ($plugin = App::instance()->plugin($pluginName)) { - $root = $plugin->root() . '/assets'; - $media = $plugin->mediaRoot(); - $assets = Dir::index($media, true); + /** + * Clean old/deprecated assets on every resolve + * + * @param string $pluginName + * @return void + */ + public static function clean(string $pluginName): void + { + if ($plugin = App::instance()->plugin($pluginName)) { + $root = $plugin->root() . '/assets'; + $media = $plugin->mediaRoot(); + $assets = Dir::index($media, true); - foreach ($assets as $asset) { - $original = $root . '/' . $asset; + foreach ($assets as $asset) { + $original = $root . '/' . $asset; - if (file_exists($original) === false) { - $assetRoot = $media . '/' . $asset; + if (file_exists($original) === false) { + $assetRoot = $media . '/' . $asset; - if (is_file($assetRoot) === true) { - F::remove($assetRoot); - } else { - Dir::remove($assetRoot); - } - } - } - } - } + if (is_file($assetRoot) === true) { + F::remove($assetRoot); + } else { + Dir::remove($assetRoot); + } + } + } + } + } - /** - * Create a symlink for a plugin asset and - * return the public URL - * - * @param string $pluginName - * @param string $filename - * @return \Kirby\Cms\Response|null - */ - public static function resolve(string $pluginName, string $filename) - { - if ($plugin = App::instance()->plugin($pluginName)) { - $source = $plugin->root() . '/assets/' . $filename; + /** + * Create a symlink for a plugin asset and + * return the public URL + * + * @param string $pluginName + * @param string $filename + * @return \Kirby\Cms\Response|null + */ + public static function resolve(string $pluginName, string $filename) + { + if ($plugin = App::instance()->plugin($pluginName)) { + $source = $plugin->root() . '/assets/' . $filename; - if (F::exists($source, $plugin->root()) === true) { - // do some spring cleaning for older files - static::clean($pluginName); + if (F::exists($source, $plugin->root()) === true) { + // do some spring cleaning for older files + static::clean($pluginName); - $target = $plugin->mediaRoot() . '/' . $filename; + $target = $plugin->mediaRoot() . '/' . $filename; - // create a symlink if possible - F::link($source, $target, 'symlink'); + // create a symlink if possible + F::link($source, $target, 'symlink'); - // return the file response - return Response::file($source); - } - } + // return the file response + return Response::file($source); + } + } - return null; - } + return null; + } } diff --git a/kirby/src/Cms/R.php b/kirby/src/Cms/R.php index ee881cf..5ef5df9 100644 --- a/kirby/src/Cms/R.php +++ b/kirby/src/Cms/R.php @@ -15,11 +15,11 @@ use Kirby\Toolkit\Facade; */ class R extends Facade { - /** - * @return \Kirby\Http\Request - */ - public static function instance() - { - return App::instance()->request(); - } + /** + * @return \Kirby\Http\Request + */ + public static function instance() + { + return App::instance()->request(); + } } diff --git a/kirby/src/Cms/Responder.php b/kirby/src/Cms/Responder.php index 360ac84..7f3d38e 100644 --- a/kirby/src/Cms/Responder.php +++ b/kirby/src/Cms/Responder.php @@ -17,297 +17,431 @@ use Kirby\Toolkit\Str; */ class Responder { - /** - * Timestamp when the response expires - * in Kirby's cache - * - * @var int|null - */ - protected $expires = null; + /** + * Timestamp when the response expires + * in Kirby's cache + * + * @var int|null + */ + protected $expires = null; - /** - * HTTP status code - * - * @var int - */ - protected $code = null; + /** + * HTTP status code + * + * @var int + */ + protected $code = null; - /** - * Response body - * - * @var string - */ - protected $body = null; + /** + * Response body + * + * @var string + */ + protected $body = null; - /** - * Flag that defines whether the current - * response can be cached by Kirby's cache - * - * @var string - */ - protected $cache = true; + /** + * Flag that defines whether the current + * response can be cached by Kirby's cache + * + * @var bool + */ + protected $cache = true; - /** - * HTTP headers - * - * @var array - */ - protected $headers = []; + /** + * HTTP headers + * + * @var array + */ + protected $headers = []; - /** - * Content type - * - * @var string - */ - protected $type = null; + /** + * Content type + * + * @var string + */ + protected $type = null; - /** - * Creates and sends the response - * - * @return string - */ - public function __toString(): string - { - return (string)$this->send(); - } + /** + * Flag that defines whether the current + * response uses the HTTP `Authorization` + * request header + * + * @var bool + */ + protected $usesAuth = false; - /** - * Setter and getter for the response body - * - * @param string|null $body - * @return string|$this - */ - public function body(string $body = null) - { - if ($body === null) { - return $this->body; - } + /** + * List of cookie names the response + * relies on + * + * @var array + */ + protected $usesCookies = []; - $this->body = $body; - return $this; - } + /** + * Creates and sends the response + * + * @return string + */ + public function __toString(): string + { + return (string)$this->send(); + } - /** - * Setter and getter for the flag that defines - * whether the current response can be cached - * by Kirby's cache - * @since 3.5.5 - * - * @param bool|null $cache - * @return bool|$this - */ - public function cache(?bool $cache = null) - { - if ($cache === null) { - return $this->cache; - } + /** + * Setter and getter for the response body + * + * @param string|null $body + * @return string|$this + */ + public function body(string $body = null) + { + if ($body === null) { + return $this->body; + } - $this->cache = $cache; - return $this; - } + $this->body = $body; + return $this; + } - /** - * Setter and getter for the cache expiry - * timestamp for Kirby's cache - * @since 3.5.5 - * - * @param int|string|null $expires Timestamp, number of minutes or time string to parse - * @param bool $override If `true`, the already defined timestamp will be overridden - * @return int|null|$this - */ - public function expires($expires = null, bool $override = false) - { - // getter - if ($expires === null && $override === false) { - return $this->expires; - } + /** + * Setter and getter for the flag that defines + * whether the current response can be cached + * by Kirby's cache + * @since 3.5.5 + * + * @param bool|null $cache + * @return bool|$this + */ + public function cache(?bool $cache = null) + { + if ($cache === null) { + // never ever cache private responses + if (static::isPrivate($this->usesAuth(), $this->usesCookies()) === true) { + return false; + } - // explicit un-setter - if ($expires === null) { - $this->expires = null; - return $this; - } + return $this->cache; + } - // normalize the value to an integer timestamp - if (is_int($expires) === true && $expires < 1000000000) { - // number of minutes - $expires = time() + ($expires * 60); - } elseif (is_int($expires) !== true) { - // time string - $parsedExpires = strtotime($expires); + $this->cache = $cache; + return $this; + } - if (is_int($parsedExpires) !== true) { - throw new InvalidArgumentException('Invalid time string "' . $expires . '"'); - } + /** + * Setter and getter for the flag that defines + * whether the current response uses the HTTP + * `Authorization` request header + * @since 3.7.0 + * + * @param bool|null $usesAuth + * @return bool|$this + */ + public function usesAuth(?bool $usesAuth = null) + { + if ($usesAuth === null) { + return $this->usesAuth; + } - $expires = $parsedExpires; - } + $this->usesAuth = $usesAuth; + return $this; + } - // by default only ever *reduce* the cache expiry time - if ( - $override === true || - $this->expires === null || - $expires < $this->expires - ) { - $this->expires = $expires; - } + /** + * Setter for a cookie name that is + * used by the response + * @since 3.7.0 + * + * @param string $name + * @return void + */ + public function usesCookie(string $name): void + { + // only add unique names + if (in_array($name, $this->usesCookies) === false) { + $this->usesCookies[] = $name; + } + } - return $this; - } + /** + * Setter and getter for the list of cookie + * names the response relies on + * @since 3.7.0 + * + * @param array|null $usesCookies + * @return array|$this + */ + public function usesCookies(?array $usesCookies = null) + { + if ($usesCookies === null) { + return $this->usesCookies; + } - /** - * Setter and getter for the status code - * - * @param int|null $code - * @return int|$this - */ - public function code(int $code = null) - { - if ($code === null) { - return $this->code; - } + $this->usesCookies = $usesCookies; + return $this; + } - $this->code = $code; - return $this; - } + /** + * Setter and getter for the cache expiry + * timestamp for Kirby's cache + * @since 3.5.5 + * + * @param int|string|null $expires Timestamp, number of minutes or time string to parse + * @param bool $override If `true`, the already defined timestamp will be overridden + * @return int|null|$this + */ + public function expires($expires = null, bool $override = false) + { + // getter + if ($expires === null && $override === false) { + return $this->expires; + } - /** - * Construct response from an array - * - * @param array $response - */ - public function fromArray(array $response): void - { - $this->body($response['body'] ?? null); - $this->expires($response['expires'] ?? null); - $this->code($response['code'] ?? null); - $this->headers($response['headers'] ?? null); - $this->type($response['type'] ?? null); - } + // explicit un-setter + if ($expires === null) { + $this->expires = null; + return $this; + } - /** - * Setter and getter for a single header - * - * @param string $key - * @param string|false|null $value - * @param bool $lazy If `true`, an existing header value is not overridden - * @return string|$this - */ - public function header(string $key, $value = null, bool $lazy = false) - { - if ($value === null) { - return $this->headers[$key] ?? null; - } + // normalize the value to an integer timestamp + if (is_int($expires) === true && $expires < 1000000000) { + // number of minutes + $expires = time() + ($expires * 60); + } elseif (is_int($expires) !== true) { + // time string + $parsedExpires = strtotime($expires); - if ($value === false) { - unset($this->headers[$key]); - return $this; - } + if (is_int($parsedExpires) !== true) { + throw new InvalidArgumentException('Invalid time string "' . $expires . '"'); + } - if ($lazy === true && isset($this->headers[$key]) === true) { - return $this; - } + $expires = $parsedExpires; + } - $this->headers[$key] = $value; - return $this; - } + // by default only ever *reduce* the cache expiry time + if ( + $override === true || + $this->expires === null || + $expires < $this->expires + ) { + $this->expires = $expires; + } - /** - * Setter and getter for all headers - * - * @param array|null $headers - * @return array|$this - */ - public function headers(array $headers = null) - { - if ($headers === null) { - return $this->headers; - } + return $this; + } - $this->headers = $headers; - return $this; - } + /** + * Setter and getter for the status code + * + * @param int|null $code + * @return int|$this + */ + public function code(int $code = null) + { + if ($code === null) { + return $this->code; + } - /** - * Shortcut to configure a json response - * - * @param array|null $json - * @return string|$this - */ - public function json(array $json = null) - { - if ($json !== null) { - $this->body(json_encode($json)); - } + $this->code = $code; + return $this; + } - return $this->type('application/json'); - } + /** + * Construct response from an array + * + * @param array $response + */ + public function fromArray(array $response): void + { + $this->body($response['body'] ?? null); + $this->cache($response['cache'] ?? null); + $this->code($response['code'] ?? null); + $this->expires($response['expires'] ?? null); + $this->headers($response['headers'] ?? null); + $this->type($response['type'] ?? null); + $this->usesAuth($response['usesAuth'] ?? null); + $this->usesCookies($response['usesCookies'] ?? null); + } - /** - * Shortcut to create a redirect response - * - * @param string|null $location - * @param int|null $code - * @return $this - */ - public function redirect(?string $location = null, ?int $code = null) - { - $location = Url::to($location ?? '/'); - $location = Url::unIdn($location); + /** + * Setter and getter for a single header + * + * @param string $key + * @param string|false|null $value + * @param bool $lazy If `true`, an existing header value is not overridden + * @return string|$this + */ + public function header(string $key, $value = null, bool $lazy = false) + { + if ($value === null) { + return $this->headers()[$key] ?? null; + } - return $this - ->header('Location', (string)$location) - ->code($code ?? 302); - } + if ($value === false) { + unset($this->headers[$key]); + return $this; + } - /** - * Creates and returns the response object from the config - * - * @param string|null $body - * @return \Kirby\Cms\Response - */ - public function send(string $body = null) - { - if ($body !== null) { - $this->body($body); - } + if ($lazy === true && isset($this->headers[$key]) === true) { + return $this; + } - return new Response($this->toArray()); - } + $this->headers[$key] = $value; + return $this; + } - /** - * Converts the response configuration - * to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'body' => $this->body, - 'code' => $this->code, - 'headers' => $this->headers, - 'type' => $this->type, - ]; - } + /** + * Setter and getter for all headers + * + * @param array|null $headers + * @return array|$this + */ + public function headers(array $headers = null) + { + if ($headers === null) { + $injectedHeaders = []; - /** - * Setter and getter for the content type - * - * @param string|null $type - * @return string|$this - */ - public function type(string $type = null) - { - if ($type === null) { - return $this->type; - } + if (static::isPrivate($this->usesAuth(), $this->usesCookies()) === true) { + // never ever cache private responses + $injectedHeaders['Cache-Control'] = 'no-store, private'; + } else { + // the response is public, but it may + // vary based on request headers + $vary = []; - if (Str::contains($type, '/') === false) { - $type = Mime::fromExtension($type); - } + if ($this->usesAuth() === true) { + $vary[] = 'Authorization'; + } - $this->type = $type; - return $this; - } + if ($this->usesCookies() !== []) { + $vary[] = 'Cookie'; + } + + if ($vary !== []) { + $injectedHeaders['Vary'] = implode(', ', $vary); + } + } + + // lazily inject (never override custom headers) + return array_merge($injectedHeaders, $this->headers); + } + + $this->headers = $headers; + return $this; + } + + /** + * Shortcut to configure a json response + * + * @param array|null $json + * @return string|$this + */ + public function json(array $json = null) + { + if ($json !== null) { + $this->body(json_encode($json)); + } + + return $this->type('application/json'); + } + + /** + * Shortcut to create a redirect response + * + * @param string|null $location + * @param int|null $code + * @return $this + */ + public function redirect(?string $location = null, ?int $code = null) + { + $location = Url::to($location ?? '/'); + $location = Url::unIdn($location); + + return $this + ->header('Location', (string)$location) + ->code($code ?? 302); + } + + /** + * Creates and returns the response object from the config + * + * @param string|null $body + * @return \Kirby\Cms\Response + */ + public function send(string $body = null) + { + if ($body !== null) { + $this->body($body); + } + + return new Response($this->toArray()); + } + + /** + * Converts the response configuration + * to an array + * + * @return array + */ + public function toArray(): array + { + // the `cache`, `expires`, `usesAuth` and `usesCookies` + // values are explicitly *not* serialized as they are + // volatile and not to be exported + return [ + 'body' => $this->body(), + 'code' => $this->code(), + 'headers' => $this->headers(), + 'type' => $this->type(), + ]; + } + + /** + * Setter and getter for the content type + * + * @param string|null $type + * @return string|$this + */ + public function type(string $type = null) + { + if ($type === null) { + return $this->type; + } + + if (Str::contains($type, '/') === false) { + $type = Mime::fromExtension($type); + } + + $this->type = $type; + return $this; + } + + /** + * Checks whether the response needs to be exempted from + * all caches due to using dynamic data based on auth + * and/or cookies; the request data only matters if it + * is actually used/relied on by the response + * @since 3.7.0 + * @internal + * + * @param bool $usesAuth + * @param array $usesCookies + * @return bool + */ + public static function isPrivate(bool $usesAuth, array $usesCookies): bool + { + $kirby = App::instance(); + + if ($usesAuth === true && $kirby->request()->hasAuth() === true) { + return true; + } + + foreach ($usesCookies as $cookie) { + if (isset($_COOKIE[$cookie]) === true) { + return true; + } + } + + return false; + } } diff --git a/kirby/src/Cms/Response.php b/kirby/src/Cms/Response.php index b4f9636..9674440 100644 --- a/kirby/src/Cms/Response.php +++ b/kirby/src/Cms/Response.php @@ -14,17 +14,17 @@ namespace Kirby\Cms; */ class Response extends \Kirby\Http\Response { - /** - * Adjusted redirect creation which - * parses locations with the Url::to method - * first. - * - * @param string $location - * @param int $code - * @return static - */ - public static function redirect(string $location = '/', int $code = 302) - { - return parent::redirect(Url::to($location), $code); - } + /** + * Adjusted redirect creation which + * parses locations with the Url::to method + * first. + * + * @param string $location + * @param int $code + * @return static + */ + public static function redirect(string $location = '/', int $code = 302) + { + return parent::redirect(Url::to($location), $code); + } } diff --git a/kirby/src/Cms/Role.php b/kirby/src/Cms/Role.php index e943864..aa22264 100644 --- a/kirby/src/Cms/Role.php +++ b/kirby/src/Cms/Role.php @@ -19,214 +19,214 @@ use Kirby\Toolkit\I18n; */ class Role extends Model { - protected $description; - protected $name; - protected $permissions; - protected $title; + protected $description; + protected $name; + protected $permissions; + protected $title; - public function __construct(array $props) - { - $this->setProperties($props); - } + public function __construct(array $props) + { + $this->setProperties($props); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * @return string - */ - public function __toString(): string - { - return $this->name(); - } + /** + * @return string + */ + public function __toString(): string + { + return $this->name(); + } - /** - * @param array $inject - * @return static - */ - public static function admin(array $inject = []) - { - try { - return static::load('admin'); - } catch (Exception $e) { - return static::factory(static::defaults()['admin'], $inject); - } - } + /** + * @param array $inject + * @return static + */ + public static function admin(array $inject = []) + { + try { + return static::load('admin'); + } catch (Exception $e) { + return static::factory(static::defaults()['admin'], $inject); + } + } - /** - * @return array - */ - protected static function defaults(): array - { - return [ - 'admin' => [ - 'name' => 'admin', - 'description' => I18n::translate('role.admin.description'), - 'title' => I18n::translate('role.admin.title'), - 'permissions' => true, - ], - 'nobody' => [ - 'name' => 'nobody', - 'description' => I18n::translate('role.nobody.description'), - 'title' => I18n::translate('role.nobody.title'), - 'permissions' => false, - ] - ]; - } + /** + * @return array + */ + protected static function defaults(): array + { + return [ + 'admin' => [ + 'name' => 'admin', + 'description' => I18n::translate('role.admin.description'), + 'title' => I18n::translate('role.admin.title'), + 'permissions' => true, + ], + 'nobody' => [ + 'name' => 'nobody', + 'description' => I18n::translate('role.nobody.description'), + 'title' => I18n::translate('role.nobody.title'), + 'permissions' => false, + ] + ]; + } - /** - * @return mixed - */ - public function description() - { - return $this->description; - } + /** + * @return mixed + */ + public function description() + { + return $this->description; + } - /** - * @param array $props - * @param array $inject - * @return static - */ - public static function factory(array $props, array $inject = []) - { - return new static($props + $inject); - } + /** + * @param array $props + * @param array $inject + * @return static + */ + public static function factory(array $props, array $inject = []) + { + return new static($props + $inject); + } - /** - * @return string - */ - public function id(): string - { - return $this->name(); - } + /** + * @return string + */ + public function id(): string + { + return $this->name(); + } - /** - * @return bool - */ - public function isAdmin(): bool - { - return $this->name() === 'admin'; - } + /** + * @return bool + */ + public function isAdmin(): bool + { + return $this->name() === 'admin'; + } - /** - * @return bool - */ - public function isNobody(): bool - { - return $this->name() === 'nobody'; - } + /** + * @return bool + */ + public function isNobody(): bool + { + return $this->name() === 'nobody'; + } - /** - * @param string $file - * @param array $inject - * @return static - */ - public static function load(string $file, array $inject = []) - { - $data = Data::read($file); - $data['name'] = F::name($file); + /** + * @param string $file + * @param array $inject + * @return static + */ + public static function load(string $file, array $inject = []) + { + $data = Data::read($file); + $data['name'] = F::name($file); - return static::factory($data, $inject); - } + return static::factory($data, $inject); + } - /** - * @return string - */ - public function name(): string - { - return $this->name; - } + /** + * @return string + */ + public function name(): string + { + return $this->name; + } - /** - * @param array $inject - * @return static - */ - public static function nobody(array $inject = []) - { - try { - return static::load('nobody'); - } catch (Exception $e) { - return static::factory(static::defaults()['nobody'], $inject); - } - } + /** + * @param array $inject + * @return static + */ + public static function nobody(array $inject = []) + { + try { + return static::load('nobody'); + } catch (Exception $e) { + return static::factory(static::defaults()['nobody'], $inject); + } + } - /** - * @return \Kirby\Cms\Permissions - */ - public function permissions() - { - return $this->permissions; - } + /** + * @return \Kirby\Cms\Permissions + */ + public function permissions() + { + return $this->permissions; + } - /** - * @param mixed $description - * @return $this - */ - protected function setDescription($description = null) - { - $this->description = I18n::translate($description, $description); - return $this; - } + /** + * @param mixed $description + * @return $this + */ + protected function setDescription($description = null) + { + $this->description = I18n::translate($description, $description); + return $this; + } - /** - * @param string $name - * @return $this - */ - protected function setName(string $name) - { - $this->name = $name; - return $this; - } + /** + * @param string $name + * @return $this + */ + protected function setName(string $name) + { + $this->name = $name; + return $this; + } - /** - * @param mixed $permissions - * @return $this - */ - protected function setPermissions($permissions = null) - { - $this->permissions = new Permissions($permissions); - return $this; - } + /** + * @param mixed $permissions + * @return $this + */ + protected function setPermissions($permissions = null) + { + $this->permissions = new Permissions($permissions); + return $this; + } - /** - * @param mixed $title - * @return $this - */ - protected function setTitle($title = null) - { - $this->title = I18n::translate($title, $title); - return $this; - } + /** + * @param mixed $title + * @return $this + */ + protected function setTitle($title = null) + { + $this->title = I18n::translate($title, $title); + return $this; + } - /** - * @return string - */ - public function title(): string - { - return $this->title ??= ucfirst($this->name()); - } + /** + * @return string + */ + public function title(): string + { + return $this->title ??= ucfirst($this->name()); + } - /** - * Converts the most important role - * properties to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'description' => $this->description(), - 'id' => $this->id(), - 'name' => $this->name(), - 'permissions' => $this->permissions()->toArray(), - 'title' => $this->title(), - ]; - } + /** + * Converts the most important role + * properties to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'description' => $this->description(), + 'id' => $this->id(), + 'name' => $this->name(), + 'permissions' => $this->permissions()->toArray(), + 'title' => $this->title(), + ]; + } } diff --git a/kirby/src/Cms/Roles.php b/kirby/src/Cms/Roles.php index 1e43804..fb99dd0 100644 --- a/kirby/src/Cms/Roles.php +++ b/kirby/src/Cms/Roles.php @@ -18,122 +18,128 @@ namespace Kirby\Cms; */ class Roles extends Collection { - /** - * Returns a filtered list of all - * roles that can be created by the - * current user - * - * @return $this|static - * @throws \Exception - */ - public function canBeChanged() - { - if (App::instance()->user()) { - return $this->filter(function ($role) { - $newUser = new User([ - 'email' => 'test@getkirby.com', - 'role' => $role->id() - ]); + /** + * Returns a filtered list of all + * roles that can be created by the + * current user + * + * @return $this|static + * @throws \Exception + */ + public function canBeChanged() + { + if (App::instance()->user()) { + return $this->filter(function ($role) { + $newUser = new User([ + 'email' => 'test@getkirby.com', + 'role' => $role->id() + ]); - return $newUser->permissions()->can('changeRole'); - }); - } + return $newUser->permissions()->can('changeRole'); + }); + } - return $this; - } + return $this; + } - /** - * Returns a filtered list of all - * roles that can be created by the - * current user - * - * @return $this|static - * @throws \Exception - */ - public function canBeCreated() - { - if (App::instance()->user()) { - return $this->filter(function ($role) { - $newUser = new User([ - 'email' => 'test@getkirby.com', - 'role' => $role->id() - ]); + /** + * Returns a filtered list of all + * roles that can be created by the + * current user + * + * @return $this|static + * @throws \Exception + */ + public function canBeCreated() + { + if (App::instance()->user()) { + return $this->filter(function ($role) { + $newUser = new User([ + 'email' => 'test@getkirby.com', + 'role' => $role->id() + ]); - return $newUser->permissions()->can('create'); - }); - } + return $newUser->permissions()->can('create'); + }); + } - return $this; - } + return $this; + } - /** - * @param array $roles - * @param array $inject - * @return static - */ - public static function factory(array $roles, array $inject = []) - { - $collection = new static(); + /** + * @param array $roles + * @param array $inject + * @return static + */ + public static function factory(array $roles, array $inject = []) + { + $collection = new static(); - // read all user blueprints - foreach ($roles as $props) { - $role = Role::factory($props, $inject); - $collection->set($role->id(), $role); - } + // read all user blueprints + foreach ($roles as $props) { + $role = Role::factory($props, $inject); + $collection->set($role->id(), $role); + } - // always include the admin role - if ($collection->find('admin') === null) { - $collection->set('admin', Role::admin()); - } + // always include the admin role + if ($collection->find('admin') === null) { + $collection->set('admin', Role::admin()); + } - // return the collection sorted by name - return $collection->sort('name', 'asc'); - } + // return the collection sorted by name + return $collection->sort('name', 'asc'); + } - /** - * @param string|null $root - * @param array $inject - * @return static - */ - public static function load(string $root = null, array $inject = []) - { - $roles = new static(); + /** + * @param string|null $root + * @param array $inject + * @return static + */ + public static function load(string $root = null, array $inject = []) + { + $kirby = App::instance(); + $roles = new static(); - // load roles from plugins - foreach (App::instance()->extensions('blueprints') as $blueprintName => $blueprint) { - if (substr($blueprintName, 0, 6) !== 'users/') { - continue; - } + // load roles from plugins + foreach ($kirby->extensions('blueprints') as $blueprintName => $blueprint) { + if (substr($blueprintName, 0, 6) !== 'users/') { + continue; + } - if (is_array($blueprint) === true) { - $role = Role::factory($blueprint, $inject); - } else { - $role = Role::load($blueprint, $inject); - } + // callback option can be return array or blueprint file path + if (is_callable($blueprint) === true) { + $blueprint = $blueprint($kirby); + } - $roles->set($role->id(), $role); - } + if (is_array($blueprint) === true) { + $role = Role::factory($blueprint, $inject); + } else { + $role = Role::load($blueprint, $inject); + } - // load roles from directory - if ($root !== null) { - foreach (glob($root . '/*.yml') as $file) { - $filename = basename($file); + $roles->set($role->id(), $role); + } - if ($filename === 'default.yml') { - continue; - } + // load roles from directory + if ($root !== null) { + foreach (glob($root . '/*.yml') as $file) { + $filename = basename($file); - $role = Role::load($file, $inject); - $roles->set($role->id(), $role); - } - } + if ($filename === 'default.yml') { + continue; + } - // always include the admin role - if ($roles->find('admin') === null) { - $roles->set('admin', Role::admin($inject)); - } + $role = Role::load($file, $inject); + $roles->set($role->id(), $role); + } + } - // return the collection sorted by name - return $roles->sort('name', 'asc'); - } + // always include the admin role + if ($roles->find('admin') === null) { + $roles->set('admin', Role::admin($inject)); + } + + // return the collection sorted by name + return $roles->sort('name', 'asc'); + } } diff --git a/kirby/src/Cms/S.php b/kirby/src/Cms/S.php index 0db9f93..cced071 100644 --- a/kirby/src/Cms/S.php +++ b/kirby/src/Cms/S.php @@ -15,11 +15,11 @@ use Kirby\Toolkit\Facade; */ class S extends Facade { - /** - * @return \Kirby\Session\Session - */ - public static function instance() - { - return App::instance()->session(); - } + /** + * @return \Kirby\Session\Session + */ + public static function instance() + { + return App::instance()->session(); + } } diff --git a/kirby/src/Cms/Search.php b/kirby/src/Cms/Search.php index 1782133..0169b40 100644 --- a/kirby/src/Cms/Search.php +++ b/kirby/src/Cms/Search.php @@ -16,47 +16,47 @@ namespace Kirby\Cms; */ class Search { - /** - * @param string|null $query - * @param array $params - * @return \Kirby\Cms\Files - */ - public static function files(string $query = null, $params = []) - { - return App::instance()->site()->index()->files()->search($query, $params); - } + /** + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Files + */ + public static function files(string $query = null, $params = []) + { + return App::instance()->site()->index()->files()->search($query, $params); + } - /** - * Native search method to search for anything within the collection - * - * @param \Kirby\Cms\Collection $collection - * @param string|null $query - * @param mixed $params - * @return \Kirby\Cms\Collection|bool - */ - public static function collection(Collection $collection, string $query = null, $params = []) - { - $kirby = App::instance(); - return ($kirby->component('search'))($kirby, $collection, $query, $params); - } + /** + * Native search method to search for anything within the collection + * + * @param \Kirby\Cms\Collection $collection + * @param string|null $query + * @param mixed $params + * @return \Kirby\Cms\Collection|bool + */ + public static function collection(Collection $collection, string $query = null, $params = []) + { + $kirby = App::instance(); + return ($kirby->component('search'))($kirby, $collection, $query, $params); + } - /** - * @param string|null $query - * @param array $params - * @return \Kirby\Cms\Pages - */ - public static function pages(string $query = null, $params = []) - { - return App::instance()->site()->index()->search($query, $params); - } + /** + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Pages + */ + public static function pages(string $query = null, $params = []) + { + return App::instance()->site()->index()->search($query, $params); + } - /** - * @param string|null $query - * @param array $params - * @return \Kirby\Cms\Users - */ - public static function users(string $query = null, $params = []) - { - return App::instance()->users()->search($query, $params); - } + /** + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Users + */ + public static function users(string $query = null, $params = []) + { + return App::instance()->users()->search($query, $params); + } } diff --git a/kirby/src/Cms/Section.php b/kirby/src/Cms/Section.php index 9c394c7..d68b3c2 100644 --- a/kirby/src/Cms/Section.php +++ b/kirby/src/Cms/Section.php @@ -16,92 +16,92 @@ use Kirby\Toolkit\Component; */ class Section extends Component { - /** - * Registry for all component mixins - * - * @var array - */ - public static $mixins = []; + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; - /** - * Registry for all component types - * - * @var array - */ - public static $types = []; + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; - /** - * Section constructor. - * - * @param string $type - * @param array $attrs - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function __construct(string $type, array $attrs = []) - { - if (isset($attrs['model']) === false) { - throw new InvalidArgumentException('Undefined section model'); - } + /** + * Section constructor. + * + * @param string $type + * @param array $attrs + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(string $type, array $attrs = []) + { + if (isset($attrs['model']) === false) { + throw new InvalidArgumentException('Undefined section model'); + } - if (is_a($attrs['model'], 'Kirby\Cms\Model') === false) { - throw new InvalidArgumentException('Invalid section model'); - } + if (is_a($attrs['model'], 'Kirby\Cms\Model') === false) { + throw new InvalidArgumentException('Invalid section model'); + } - // use the type as fallback for the name - $attrs['name'] ??= $type; - $attrs['type'] = $type; + // use the type as fallback for the name + $attrs['name'] ??= $type; + $attrs['type'] = $type; - parent::__construct($type, $attrs); - } + parent::__construct($type, $attrs); + } - public function errors(): array - { - if (array_key_exists('errors', $this->methods) === true) { - return $this->methods['errors']->call($this); - } + public function errors(): array + { + if (array_key_exists('errors', $this->methods) === true) { + return $this->methods['errors']->call($this); + } - return $this->errors ?? []; - } + return $this->errors ?? []; + } - /** - * @return \Kirby\Cms\App - */ - public function kirby() - { - return $this->model()->kirby(); - } + /** + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model()->kirby(); + } - /** - * @return \Kirby\Cms\Model - */ - public function model() - { - return $this->model; - } + /** + * @return \Kirby\Cms\Model + */ + public function model() + { + return $this->model; + } - /** - * @return array - */ - public function toArray(): array - { - $array = parent::toArray(); + /** + * @return array + */ + public function toArray(): array + { + $array = parent::toArray(); - unset($array['model']); + unset($array['model']); - return $array; - } + return $array; + } - /** - * @return array - */ - public function toResponse(): array - { - return array_merge([ - 'status' => 'ok', - 'code' => 200, - 'name' => $this->name, - 'type' => $this->type - ], $this->toArray()); - } + /** + * @return array + */ + public function toResponse(): array + { + return array_merge([ + 'status' => 'ok', + 'code' => 200, + 'name' => $this->name, + 'type' => $this->type + ], $this->toArray()); + } } diff --git a/kirby/src/Cms/Site.php b/kirby/src/Cms/Site.php index c546fd0..2343ed7 100644 --- a/kirby/src/Cms/Site.php +++ b/kirby/src/Cms/Site.php @@ -22,678 +22,678 @@ use Kirby\Toolkit\A; */ class Site extends ModelWithContent { - use SiteActions; - use HasChildren; - use HasFiles; - use HasMethods; - - public const CLASS_ALIAS = 'site'; - - /** - * The SiteBlueprint object - * - * @var \Kirby\Cms\SiteBlueprint - */ - protected $blueprint; - - /** - * The error page object - * - * @var \Kirby\Cms\Page - */ - protected $errorPage; - - /** - * The id of the error page, which is - * fetched in the errorPage method - * - * @var string - */ - protected $errorPageId = 'error'; - - /** - * The home page object - * - * @var \Kirby\Cms\Page - */ - protected $homePage; - - /** - * The id of the home page, which is - * fetched in the errorPage method - * - * @var string - */ - protected $homePageId = 'home'; - - /** - * Cache for the inventory array - * - * @var array - */ - protected $inventory; - - /** - * The current page object - * - * @var \Kirby\Cms\Page - */ - protected $page; - - /** - * The absolute path to the site directory - * - * @var string - */ - protected $root; - - /** - * The page url - * - * @var string - */ - protected $url; - - /** - * Modified getter to also return fields - * from the content - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } - - // site methods - if ($this->hasMethod($method)) { - return $this->callMethod($method, $arguments); - } - - // return site content otherwise - return $this->content()->get($method); - } - - /** - * Creates a new Site object - * - * @param array $props - */ - public function __construct(array $props = []) - { - $this->setProperties($props); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return array_merge($this->toArray(), [ - 'content' => $this->content(), - 'children' => $this->children(), - 'files' => $this->files(), - ]); - } - - /** - * Makes it possible to convert the site model - * to a string. Mostly useful for debugging. - * - * @return string - */ - public function __toString(): string - { - return $this->url(); - } - - /** - * Returns the url to the api endpoint - * - * @internal - * @param bool $relative - * @return string - */ - public function apiUrl(bool $relative = false): string - { - if ($relative === true) { - return 'site'; - } else { - return $this->kirby()->url('api') . '/site'; - } - } - - /** - * Returns the blueprint object - * - * @return \Kirby\Cms\SiteBlueprint - */ - public function blueprint() - { - if (is_a($this->blueprint, 'Kirby\Cms\SiteBlueprint') === true) { - return $this->blueprint; - } - - return $this->blueprint = SiteBlueprint::factory('site', null, $this); - } - - /** - * Builds a breadcrumb collection - * - * @return \Kirby\Cms\Pages - */ - public function breadcrumb() - { - // get all parents and flip the order - $crumb = $this->page()->parents()->flip(); - - // add the home page - $crumb->prepend($this->homePage()->id(), $this->homePage()); - - // add the active page - $crumb->append($this->page()->id(), $this->page()); - - return $crumb; - } - - /** - * Prepares the content for the write method - * - * @internal - * @param array $data - * @param string|null $languageCode - * @return array - */ - public function contentFileData(array $data, ?string $languageCode = null): array - { - return A::prepend($data, [ - 'title' => $data['title'] ?? null, - ]); - } - - /** - * Filename for the content file - * - * @internal - * @return string - */ - public function contentFileName(): string - { - return 'site'; - } - - /** - * Returns the error page object - * - * @return \Kirby\Cms\Page|null - */ - public function errorPage() - { - if (is_a($this->errorPage, 'Kirby\Cms\Page') === true) { - return $this->errorPage; - } - - if ($error = $this->find($this->errorPageId())) { - return $this->errorPage = $error; - } - - return null; - } - - /** - * Returns the global error page id - * - * @internal - * @return string - */ - public function errorPageId(): string - { - return $this->errorPageId ?? 'error'; - } - - /** - * Checks if the site exists on disk - * - * @return bool - */ - public function exists(): bool - { - return is_dir($this->root()) === true; - } - - /** - * Returns the home page object - * - * @return \Kirby\Cms\Page|null - */ - public function homePage() - { - if (is_a($this->homePage, 'Kirby\Cms\Page') === true) { - return $this->homePage; - } - - if ($home = $this->find($this->homePageId())) { - return $this->homePage = $home; - } - - return null; - } - - /** - * Returns the global home page id - * - * @internal - * @return string - */ - public function homePageId(): string - { - return $this->homePageId ?? 'home'; - } - - /** - * Creates an inventory of all files - * and children in the site directory - * - * @internal - * @return array - */ - public function inventory(): array - { - if ($this->inventory !== null) { - return $this->inventory; - } - - $kirby = $this->kirby(); - - return $this->inventory = Dir::inventory( - $this->root(), - $kirby->contentExtension(), - $kirby->contentIgnore(), - $kirby->multilang() - ); - } - - /** - * Compares the current object with the given site object - * - * @param mixed $site - * @return bool - */ - public function is($site): bool - { - if (is_a($site, 'Kirby\Cms\Site') === false) { - return false; - } - - return $this === $site; - } - - /** - * Returns the root to the media folder for the site - * - * @internal - * @return string - */ - public function mediaRoot(): string - { - return $this->kirby()->root('media') . '/site'; - } - - /** - * The site's base url for any files - * - * @internal - * @return string - */ - public function mediaUrl(): string - { - return $this->kirby()->url('media') . '/site'; - } - - /** - * Gets the last modification date of all pages - * in the content folder. - * - * @param string|null $format - * @param string|null $handler - * @return int|string - */ - public function modified(?string $format = null, ?string $handler = null) - { - return Dir::modified( - $this->root(), - $format, - $handler ?? $this->kirby()->option('date.handler', 'date') - ); - } - - /** - * Returns the current page if `$path` - * is not specified. Otherwise it will try - * to find a page by the given path. - * - * If no current page is set with the page - * prop, the home page will be returned if - * it can be found. (see `Site::homePage()`) - * - * @param string|null $path omit for current page, - * otherwise e.g. `notes/across-the-ocean` - * @return \Kirby\Cms\Page|null - */ - public function page(?string $path = null) - { - if ($path !== null) { - return $this->find($path); - } - - if (is_a($this->page, 'Kirby\Cms\Page') === true) { - return $this->page; - } - - try { - return $this->page = $this->homePage(); - } catch (LogicException $e) { - return $this->page = null; - } - } - - /** - * Alias for `Site::children()` - * - * @return \Kirby\Cms\Pages - */ - public function pages() - { - return $this->children(); - } - - /** - * Returns the panel info object - * - * @return \Kirby\Panel\Site - */ - public function panel() - { - return new Panel($this); - } - - /** - * Returns the permissions object for this site - * - * @return \Kirby\Cms\SitePermissions - */ - public function permissions() - { - return new SitePermissions($this); - } - - /** - * Preview Url - * - * @internal - * @return string|null - */ - public function previewUrl(): ?string - { - $preview = $this->blueprint()->preview(); - - if ($preview === false) { - return null; - } - - if ($preview === true) { - $url = $this->url(); - } else { - $url = $preview; - } - - return $url; - } - - /** - * Returns the absolute path to the content directory - * - * @return string - */ - public function root(): string - { - return $this->root ??= $this->kirby()->root('content'); - } - - /** - * Returns the SiteRules class instance - * which is being used in various methods - * to check for valid actions and input. - * - * @return \Kirby\Cms\SiteRules - */ - protected function rules() - { - return new SiteRules(); - } - - /** - * Search all pages in the site - * - * @param string|null $query - * @param array $params - * @return \Kirby\Cms\Pages - */ - public function search(?string $query = null, $params = []) - { - return $this->index()->search($query, $params); - } - - /** - * Sets the Blueprint object - * - * @param array|null $blueprint - * @return $this - */ - protected function setBlueprint(?array $blueprint = null) - { - if ($blueprint !== null) { - $blueprint['model'] = $this; - $this->blueprint = new SiteBlueprint($blueprint); - } - - return $this; - } - - /** - * Sets the id of the error page, which - * is used in the errorPage method - * to get the default error page if nothing - * else is set. - * - * @param string $id - * @return $this - */ - protected function setErrorPageId(string $id = 'error') - { - $this->errorPageId = $id; - return $this; - } - - /** - * Sets the id of the home page, which - * is used in the homePage method - * to get the default home page if nothing - * else is set. - * - * @param string $id - * @return $this - */ - protected function setHomePageId(string $id = 'home') - { - $this->homePageId = $id; - return $this; - } - - /** - * Sets the current page object - * - * @internal - * @param \Kirby\Cms\Page|null $page - * @return $this - */ - public function setPage(?Page $page = null) - { - $this->page = $page; - return $this; - } - - /** - * Sets the Url - * - * @param string|null $url - * @return $this - */ - protected function setUrl(?string $url = null) - { - $this->url = $url; - return $this; - } - - /** - * Converts the most important site - * properties to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'children' => $this->children()->keys(), - 'content' => $this->content()->toArray(), - 'errorPage' => $this->errorPage() ? $this->errorPage()->id() : false, - 'files' => $this->files()->keys(), - 'homePage' => $this->homePage() ? $this->homePage()->id() : false, - 'page' => $this->page() ? $this->page()->id() : false, - 'title' => $this->title()->value(), - 'url' => $this->url(), - ]; - } - - /** - * Returns the Url - * - * @param string|null $language - * @return string - */ - public function url(?string $language = null): string - { - if ($language !== null || $this->kirby()->multilang() === true) { - return $this->urlForLanguage($language); - } - - return $this->url ?? $this->kirby()->url(); - } - - /** - * Returns the translated url - * - * @internal - * @param string|null $languageCode - * @param array|null $options - * @return string - */ - public function urlForLanguage(?string $languageCode = null, ?array $options = null): string - { - if ($language = $this->kirby()->language($languageCode)) { - return $language->url(); - } - - return $this->kirby()->url(); - } - - /** - * Sets the current page by - * id or page object and - * returns the current page - * - * @internal - * @param string|\Kirby\Cms\Page $page - * @param string|null $languageCode - * @return \Kirby\Cms\Page - */ - public function visit($page, ?string $languageCode = null) - { - if ($languageCode !== null) { - $this->kirby()->setCurrentTranslation($languageCode); - $this->kirby()->setCurrentLanguage($languageCode); - } - - // convert ids to a Page object - if (is_string($page)) { - $page = $this->find($page); - } - - // handle invalid pages - if (is_a($page, 'Kirby\Cms\Page') === false) { - throw new InvalidArgumentException('Invalid page object'); - } - - // set the current active page - $this->setPage($page); - - // return the page - return $page; - } - - /** - * Checks if any content of the site has been - * modified after the given unix timestamp - * This is mainly used to auto-update the cache - * - * @param mixed $time - * @return bool - */ - public function wasModifiedAfter($time): bool - { - return Dir::wasModifiedAfter($this->root(), $time); - } - - - /** - * Deprecated! - */ - - /** - * Returns the full path without leading slash - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @return string - * @codeCoverageIgnore - */ - public function panelPath(): string - { - return $this->panel()->path(); - } - - /** - * Returns the url to the editing view - * in the panel - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param bool $relative - * @return string - * @codeCoverageIgnore - */ - public function panelUrl(bool $relative = false): string - { - return $this->panel()->url($relative); - } + use SiteActions; + use HasChildren; + use HasFiles; + use HasMethods; + + public const CLASS_ALIAS = 'site'; + + /** + * The SiteBlueprint object + * + * @var \Kirby\Cms\SiteBlueprint + */ + protected $blueprint; + + /** + * The error page object + * + * @var \Kirby\Cms\Page + */ + protected $errorPage; + + /** + * The id of the error page, which is + * fetched in the errorPage method + * + * @var string + */ + protected $errorPageId = 'error'; + + /** + * The home page object + * + * @var \Kirby\Cms\Page + */ + protected $homePage; + + /** + * The id of the home page, which is + * fetched in the errorPage method + * + * @var string + */ + protected $homePageId = 'home'; + + /** + * Cache for the inventory array + * + * @var array + */ + protected $inventory; + + /** + * The current page object + * + * @var \Kirby\Cms\Page + */ + protected $page; + + /** + * The absolute path to the site directory + * + * @var string + */ + protected $root; + + /** + * The page url + * + * @var string + */ + protected $url; + + /** + * Modified getter to also return fields + * from the content + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // site methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // return site content otherwise + return $this->content()->get($method); + } + + /** + * Creates a new Site object + * + * @param array $props + */ + public function __construct(array $props = []) + { + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'content' => $this->content(), + 'children' => $this->children(), + 'files' => $this->files(), + ]); + } + + /** + * Makes it possible to convert the site model + * to a string. Mostly useful for debugging. + * + * @return string + */ + public function __toString(): string + { + return $this->url(); + } + + /** + * Returns the url to the api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + if ($relative === true) { + return 'site'; + } else { + return $this->kirby()->url('api') . '/site'; + } + } + + /** + * Returns the blueprint object + * + * @return \Kirby\Cms\SiteBlueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\SiteBlueprint') === true) { + return $this->blueprint; + } + + return $this->blueprint = SiteBlueprint::factory('site', null, $this); + } + + /** + * Builds a breadcrumb collection + * + * @return \Kirby\Cms\Pages + */ + public function breadcrumb() + { + // get all parents and flip the order + $crumb = $this->page()->parents()->flip(); + + // add the home page + $crumb->prepend($this->homePage()->id(), $this->homePage()); + + // add the active page + $crumb->append($this->page()->id(), $this->page()); + + return $crumb; + } + + /** + * Prepares the content for the write method + * + * @internal + * @param array $data + * @param string|null $languageCode + * @return array + */ + public function contentFileData(array $data, ?string $languageCode = null): array + { + return A::prepend($data, [ + 'title' => $data['title'] ?? null, + ]); + } + + /** + * Filename for the content file + * + * @internal + * @return string + */ + public function contentFileName(): string + { + return 'site'; + } + + /** + * Returns the error page object + * + * @return \Kirby\Cms\Page|null + */ + public function errorPage() + { + if (is_a($this->errorPage, 'Kirby\Cms\Page') === true) { + return $this->errorPage; + } + + if ($error = $this->find($this->errorPageId())) { + return $this->errorPage = $error; + } + + return null; + } + + /** + * Returns the global error page id + * + * @internal + * @return string + */ + public function errorPageId(): string + { + return $this->errorPageId ?? 'error'; + } + + /** + * Checks if the site exists on disk + * + * @return bool + */ + public function exists(): bool + { + return is_dir($this->root()) === true; + } + + /** + * Returns the home page object + * + * @return \Kirby\Cms\Page|null + */ + public function homePage() + { + if (is_a($this->homePage, 'Kirby\Cms\Page') === true) { + return $this->homePage; + } + + if ($home = $this->find($this->homePageId())) { + return $this->homePage = $home; + } + + return null; + } + + /** + * Returns the global home page id + * + * @internal + * @return string + */ + public function homePageId(): string + { + return $this->homePageId ?? 'home'; + } + + /** + * Creates an inventory of all files + * and children in the site directory + * + * @internal + * @return array + */ + public function inventory(): array + { + if ($this->inventory !== null) { + return $this->inventory; + } + + $kirby = $this->kirby(); + + return $this->inventory = Dir::inventory( + $this->root(), + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + } + + /** + * Compares the current object with the given site object + * + * @param mixed $site + * @return bool + */ + public function is($site): bool + { + if (is_a($site, 'Kirby\Cms\Site') === false) { + return false; + } + + return $this === $site; + } + + /** + * Returns the root to the media folder for the site + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/site'; + } + + /** + * The site's base url for any files + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/site'; + } + + /** + * Gets the last modification date of all pages + * in the content folder. + * + * @param string|null $format + * @param string|null $handler + * @return int|string + */ + public function modified(?string $format = null, ?string $handler = null) + { + return Dir::modified( + $this->root(), + $format, + $handler ?? $this->kirby()->option('date.handler', 'date') + ); + } + + /** + * Returns the current page if `$path` + * is not specified. Otherwise it will try + * to find a page by the given path. + * + * If no current page is set with the page + * prop, the home page will be returned if + * it can be found. (see `Site::homePage()`) + * + * @param string|null $path omit for current page, + * otherwise e.g. `notes/across-the-ocean` + * @return \Kirby\Cms\Page|null + */ + public function page(?string $path = null) + { + if ($path !== null) { + return $this->find($path); + } + + if (is_a($this->page, 'Kirby\Cms\Page') === true) { + return $this->page; + } + + try { + return $this->page = $this->homePage(); + } catch (LogicException $e) { + return $this->page = null; + } + } + + /** + * Alias for `Site::children()` + * + * @return \Kirby\Cms\Pages + */ + public function pages() + { + return $this->children(); + } + + /** + * Returns the panel info object + * + * @return \Kirby\Panel\Site + */ + public function panel() + { + return new Panel($this); + } + + /** + * Returns the permissions object for this site + * + * @return \Kirby\Cms\SitePermissions + */ + public function permissions() + { + return new SitePermissions($this); + } + + /** + * Preview Url + * + * @internal + * @return string|null + */ + public function previewUrl(): ?string + { + $preview = $this->blueprint()->preview(); + + if ($preview === false) { + return null; + } + + if ($preview === true) { + $url = $this->url(); + } else { + $url = $preview; + } + + return $url; + } + + /** + * Returns the absolute path to the content directory + * + * @return string + */ + public function root(): string + { + return $this->root ??= $this->kirby()->root('content'); + } + + /** + * Returns the SiteRules class instance + * which is being used in various methods + * to check for valid actions and input. + * + * @return \Kirby\Cms\SiteRules + */ + protected function rules() + { + return new SiteRules(); + } + + /** + * Search all pages in the site + * + * @param string|null $query + * @param array $params + * @return \Kirby\Cms\Pages + */ + public function search(?string $query = null, $params = []) + { + return $this->index()->search($query, $params); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return $this + */ + protected function setBlueprint(?array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new SiteBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the id of the error page, which + * is used in the errorPage method + * to get the default error page if nothing + * else is set. + * + * @param string $id + * @return $this + */ + protected function setErrorPageId(string $id = 'error') + { + $this->errorPageId = $id; + return $this; + } + + /** + * Sets the id of the home page, which + * is used in the homePage method + * to get the default home page if nothing + * else is set. + * + * @param string $id + * @return $this + */ + protected function setHomePageId(string $id = 'home') + { + $this->homePageId = $id; + return $this; + } + + /** + * Sets the current page object + * + * @internal + * @param \Kirby\Cms\Page|null $page + * @return $this + */ + public function setPage(?Page $page = null) + { + $this->page = $page; + return $this; + } + + /** + * Sets the Url + * + * @param string|null $url + * @return $this + */ + protected function setUrl(?string $url = null) + { + $this->url = $url; + return $this; + } + + /** + * Converts the most important site + * properties to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'children' => $this->children()->keys(), + 'content' => $this->content()->toArray(), + 'errorPage' => $this->errorPage() ? $this->errorPage()->id() : false, + 'files' => $this->files()->keys(), + 'homePage' => $this->homePage() ? $this->homePage()->id() : false, + 'page' => $this->page() ? $this->page()->id() : false, + 'title' => $this->title()->value(), + 'url' => $this->url(), + ]; + } + + /** + * Returns the Url + * + * @param string|null $language + * @return string + */ + public function url(?string $language = null): string + { + if ($language !== null || $this->kirby()->multilang() === true) { + return $this->urlForLanguage($language); + } + + return $this->url ?? $this->kirby()->url(); + } + + /** + * Returns the translated url + * + * @internal + * @param string|null $languageCode + * @param array|null $options + * @return string + */ + public function urlForLanguage(?string $languageCode = null, ?array $options = null): string + { + if ($language = $this->kirby()->language($languageCode)) { + return $language->url(); + } + + return $this->kirby()->url(); + } + + /** + * Sets the current page by + * id or page object and + * returns the current page + * + * @internal + * @param string|\Kirby\Cms\Page $page + * @param string|null $languageCode + * @return \Kirby\Cms\Page + */ + public function visit($page, ?string $languageCode = null) + { + if ($languageCode !== null) { + $this->kirby()->setCurrentTranslation($languageCode); + $this->kirby()->setCurrentLanguage($languageCode); + } + + // convert ids to a Page object + if (is_string($page)) { + $page = $this->find($page); + } + + // handle invalid pages + if (is_a($page, 'Kirby\Cms\Page') === false) { + throw new InvalidArgumentException('Invalid page object'); + } + + // set the current active page + $this->setPage($page); + + // return the page + return $page; + } + + /** + * Checks if any content of the site has been + * modified after the given unix timestamp + * This is mainly used to auto-update the cache + * + * @param mixed $time + * @return bool + */ + public function wasModifiedAfter($time): bool + { + return Dir::wasModifiedAfter($this->root(), $time); + } + + + /** + * Deprecated! + */ + + /** + * Returns the full path without leading slash + * + * @todo Remove in 3.8.0 + * + * @internal + * @return string + * @codeCoverageIgnore + */ + public function panelPath(): string + { + Helpers::deprecated('Cms\Site::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $site->panel()->path() instead.'); + return $this->panel()->path(); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @todo Remove in 3.8.0 + * + * @internal + * @param bool $relative + * @return string + * @codeCoverageIgnore + */ + public function panelUrl(bool $relative = false): string + { + Helpers::deprecated('Cms\Site::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $site->panel()->url() instead.'); + return $this->panel()->url($relative); + } } diff --git a/kirby/src/Cms/SiteActions.php b/kirby/src/Cms/SiteActions.php index 5805d9c..dd1ceb9 100644 --- a/kirby/src/Cms/SiteActions.php +++ b/kirby/src/Cms/SiteActions.php @@ -15,87 +15,87 @@ use Closure; */ trait SiteActions { - /** - * Commits a site action, by following these steps - * - * 1. checks the action rules - * 2. sends the before hook - * 3. commits the store action - * 4. sends the after hook - * 5. returns the result - * - * @param string $action - * @param mixed ...$arguments - * @param Closure $callback - * @return mixed - */ - protected function commit(string $action, array $arguments, Closure $callback) - { - $old = $this->hardcopy(); - $kirby = $this->kirby(); - $argumentValues = array_values($arguments); + /** + * Commits a site action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the store action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param mixed ...$arguments + * @param Closure $callback + * @return mixed + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); - $this->rules()->$action(...$argumentValues); - $kirby->trigger('site.' . $action . ':before', $arguments); + $this->rules()->$action(...$argumentValues); + $kirby->trigger('site.' . $action . ':before', $arguments); - $result = $callback(...$argumentValues); + $result = $callback(...$argumentValues); - $kirby->trigger('site.' . $action . ':after', ['newSite' => $result, 'oldSite' => $old]); + $kirby->trigger('site.' . $action . ':after', ['newSite' => $result, 'oldSite' => $old]); - $kirby->cache('pages')->flush(); - return $result; - } + $kirby->cache('pages')->flush(); + return $result; + } - /** - * Change the site title - * - * @param string $title - * @param string|null $languageCode - * @return static - */ - public function changeTitle(string $title, string $languageCode = null) - { - $site = $this; - $title = trim($title); - $arguments = compact('site', 'title', 'languageCode'); + /** + * Change the site title + * + * @param string $title + * @param string|null $languageCode + * @return static + */ + public function changeTitle(string $title, string $languageCode = null) + { + $site = $this; + $title = trim($title); + $arguments = compact('site', 'title', 'languageCode'); - return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) { - return $site->save(['title' => $title], $languageCode); - }); - } + return $this->commit('changeTitle', $arguments, function ($site, $title, $languageCode) { + return $site->save(['title' => $title], $languageCode); + }); + } - /** - * Creates a main page - * - * @param array $props - * @return \Kirby\Cms\Page - */ - public function createChild(array $props) - { - $props = array_merge($props, [ - 'url' => null, - 'num' => null, - 'parent' => null, - 'site' => $this, - ]); + /** + * Creates a main page + * + * @param array $props + * @return \Kirby\Cms\Page + */ + public function createChild(array $props) + { + $props = array_merge($props, [ + 'url' => null, + 'num' => null, + 'parent' => null, + 'site' => $this, + ]); - return Page::create($props); - } + return Page::create($props); + } - /** - * Clean internal caches - * - * @return $this - */ - public function purge() - { - $this->blueprint = null; - $this->children = null; - $this->content = null; - $this->files = null; - $this->inventory = null; - $this->translations = null; + /** + * Clean internal caches + * + * @return $this + */ + public function purge() + { + $this->blueprint = null; + $this->children = null; + $this->content = null; + $this->files = null; + $this->inventory = null; + $this->translations = null; - return $this; - } + return $this; + } } diff --git a/kirby/src/Cms/SiteBlueprint.php b/kirby/src/Cms/SiteBlueprint.php index 0484ba7..ce38ab9 100644 --- a/kirby/src/Cms/SiteBlueprint.php +++ b/kirby/src/Cms/SiteBlueprint.php @@ -14,47 +14,47 @@ namespace Kirby\Cms; */ class SiteBlueprint extends Blueprint { - /** - * Creates a new page blueprint object - * with the given props - * - * @param array $props - */ - public function __construct(array $props) - { - parent::__construct($props); + /** + * Creates a new page blueprint object + * with the given props + * + * @param array $props + */ + public function __construct(array $props) + { + parent::__construct($props); - // normalize all available page options - $this->props['options'] = $this->normalizeOptions( - $props['options'] ?? true, - // defaults - [ - 'changeTitle' => null, - 'update' => null, - ], - // aliases - [ - 'title' => 'changeTitle', - ] - ); - } + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $this->props['options'] ?? true, + // defaults + [ + 'changeTitle' => null, + 'update' => null, + ], + // aliases + [ + 'title' => 'changeTitle', + ] + ); + } - /** - * Returns the preview settings - * The preview setting controls the "Open" - * button in the panel and redirects it to a - * different URL if necessary. - * - * @return string|bool - */ - public function preview() - { - $preview = $this->props['options']['preview'] ?? true; + /** + * Returns the preview settings + * The preview setting controls the "Open" + * button in the panel and redirects it to a + * different URL if necessary. + * + * @return string|bool + */ + public function preview() + { + $preview = $this->props['options']['preview'] ?? true; - if (is_string($preview) === true) { - return $this->model->toString($preview); - } + if (is_string($preview) === true) { + return $this->model->toString($preview); + } - return $preview; - } + return $preview; + } } diff --git a/kirby/src/Cms/SitePermissions.php b/kirby/src/Cms/SitePermissions.php index 5ced409..b6ce350 100644 --- a/kirby/src/Cms/SitePermissions.php +++ b/kirby/src/Cms/SitePermissions.php @@ -13,5 +13,5 @@ namespace Kirby\Cms; */ class SitePermissions extends ModelPermissions { - protected $category = 'site'; + protected $category = 'site'; } diff --git a/kirby/src/Cms/SiteRules.php b/kirby/src/Cms/SiteRules.php index 64b64f4..07db1b9 100644 --- a/kirby/src/Cms/SiteRules.php +++ b/kirby/src/Cms/SiteRules.php @@ -17,42 +17,42 @@ use Kirby\Toolkit\Str; */ class SiteRules { - /** - * Validates if the site title can be changed - * - * @param \Kirby\Cms\Site $site - * @param string $title - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the title is empty - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title - */ - public static function changeTitle(Site $site, string $title): bool - { - if ($site->permissions()->changeTitle() !== true) { - throw new PermissionException(['key' => 'site.changeTitle.permission']); - } + /** + * Validates if the site title can be changed + * + * @param \Kirby\Cms\Site $site + * @param string $title + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the title is empty + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the title + */ + public static function changeTitle(Site $site, string $title): bool + { + if ($site->permissions()->changeTitle() !== true) { + throw new PermissionException(['key' => 'site.changeTitle.permission']); + } - if (Str::length($title) === 0) { - throw new InvalidArgumentException(['key' => 'site.changeTitle.empty']); - } + if (Str::length($title) === 0) { + throw new InvalidArgumentException(['key' => 'site.changeTitle.empty']); + } - return true; - } + return true; + } - /** - * Validates if the site can be updated - * - * @param \Kirby\Cms\Site $site - * @param array $content - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the site - */ - public static function update(Site $site, array $content = []): bool - { - if ($site->permissions()->update() !== true) { - throw new PermissionException(['key' => 'site.update.permission']); - } + /** + * Validates if the site can be updated + * + * @param \Kirby\Cms\Site $site + * @param array $content + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to update the site + */ + public static function update(Site $site, array $content = []): bool + { + if ($site->permissions()->update() !== true) { + throw new PermissionException(['key' => 'site.update.permission']); + } - return true; - } + return true; + } } diff --git a/kirby/src/Cms/Structure.php b/kirby/src/Cms/Structure.php index 886fff0..d86e611 100644 --- a/kirby/src/Cms/Structure.php +++ b/kirby/src/Cms/Structure.php @@ -20,45 +20,47 @@ use Kirby\Exception\InvalidArgumentException; */ class Structure extends Collection { - /** - * Creates a new Collection with the given objects - * - * @param array $objects Kirby\Cms\StructureObject` objects or props arrays - * @param object|null $parent - */ - public function __construct($objects = [], $parent = null) - { - $this->parent = $parent; - $this->set($objects); - } + /** + * Creates a new Collection with the given objects + * + * @param array $objects Kirby\Cms\StructureObject` objects or props arrays + * @param object|null $parent + */ + public function __construct($objects = [], $parent = null) + { + $this->parent = $parent; + $this->set($objects); + } - /** - * The internal setter for collection items. - * This makes sure that nothing unexpected ends - * up in the collection. You can pass arrays or - * StructureObjects - * - * @param string $id - * @param array|StructureObject $props - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function __set(string $id, $props) - { - if (is_a($props, 'Kirby\Cms\StructureObject') === true) { - $object = $props; - } else { - if (is_array($props) === false) { - throw new InvalidArgumentException('Invalid structure data'); - } + /** + * The internal setter for collection items. + * This makes sure that nothing unexpected ends + * up in the collection. You can pass arrays or + * StructureObjects + * + * @param string $id + * @param array|StructureObject $props + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __set(string $id, $props): void + { + if (is_a($props, 'Kirby\Cms\StructureObject') === true) { + $object = $props; + } else { + if (is_array($props) === false) { + throw new InvalidArgumentException('Invalid structure data'); + } - $object = new StructureObject([ - 'content' => $props, - 'id' => $props['id'] ?? $id, - 'parent' => $this->parent, - 'structure' => $this - ]); - } + $object = new StructureObject([ + 'content' => $props, + 'id' => $props['id'] ?? $id, + 'parent' => $this->parent, + 'structure' => $this + ]); + } - return parent::__set($object->id(), $object); - } + parent::__set($object->id(), $object); + } } diff --git a/kirby/src/Cms/StructureObject.php b/kirby/src/Cms/StructureObject.php index 877c798..2b0bddd 100644 --- a/kirby/src/Cms/StructureObject.php +++ b/kirby/src/Cms/StructureObject.php @@ -20,190 +20,190 @@ namespace Kirby\Cms; */ class StructureObject extends Model { - use HasSiblings; + use HasSiblings; - /** - * The content - * - * @var Content - */ - protected $content; + /** + * The content + * + * @var Content + */ + protected $content; - /** - * @var string - */ - protected $id; + /** + * @var string + */ + protected $id; - /** - * @var \Kirby\Cms\Site|\Kirby\Cms\Page|\Kirby\Cms\File|\Kirby\Cms\User|null - */ - protected $parent; + /** + * @var \Kirby\Cms\Site|\Kirby\Cms\Page|\Kirby\Cms\File|\Kirby\Cms\User|null + */ + protected $parent; - /** - * The parent Structure collection - * - * @var Structure - */ - protected $structure; + /** + * The parent Structure collection + * + * @var Structure + */ + protected $structure; - /** - * Modified getter to also return fields - * from the object's content - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } + /** + * Modified getter to also return fields + * from the object's content + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } - return $this->content()->get($method); - } + return $this->content()->get($method); + } - /** - * Creates a new StructureObject with the given props - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setProperties($props); - } + /** + * Creates a new StructureObject with the given props + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } - /** - * Returns the content - * - * @return \Kirby\Cms\Content - */ - public function content() - { - if (is_a($this->content, 'Kirby\Cms\Content') === true) { - return $this->content; - } + /** + * Returns the content + * + * @return \Kirby\Cms\Content + */ + public function content() + { + if (is_a($this->content, 'Kirby\Cms\Content') === true) { + return $this->content; + } - if (is_array($this->content) !== true) { - $this->content = []; - } + if (is_array($this->content) !== true) { + $this->content = []; + } - return $this->content = new Content($this->content, $this->parent()); - } + return $this->content = new Content($this->content, $this->parent()); + } - /** - * Returns the required id - * - * @return string - */ - public function id(): string - { - return $this->id; - } + /** + * Returns the required id + * + * @return string + */ + public function id(): string + { + return $this->id; + } - /** - * Compares the current object with the given structure object - * - * @param mixed $structure - * @return bool - */ - public function is($structure): bool - { - if (is_a($structure, 'Kirby\Cms\StructureObject') === false) { - return false; - } + /** + * Compares the current object with the given structure object + * + * @param mixed $structure + * @return bool + */ + public function is($structure): bool + { + if (is_a($structure, 'Kirby\Cms\StructureObject') === false) { + return false; + } - return $this === $structure; - } + return $this === $structure; + } - /** - * Returns the parent Model object - * - * @return \Kirby\Cms\Model - */ - public function parent() - { - return $this->parent; - } + /** + * Returns the parent Model object + * + * @return \Kirby\Cms\Model + */ + public function parent() + { + return $this->parent; + } - /** - * Sets the Content object with the given parent - * - * @param array|null $content - * @return $this - */ - protected function setContent(array $content = null) - { - $this->content = $content; - return $this; - } + /** + * Sets the Content object with the given parent + * + * @param array|null $content + * @return $this + */ + protected function setContent(array $content = null) + { + $this->content = $content; + return $this; + } - /** - * Sets the id of the object. - * The id is required. The structure - * class will use the index, if no id is - * specified. - * - * @param string $id - * @return $this - */ - protected function setId(string $id) - { - $this->id = $id; - return $this; - } + /** + * Sets the id of the object. + * The id is required. The structure + * class will use the index, if no id is + * specified. + * + * @param string $id + * @return $this + */ + protected function setId(string $id) + { + $this->id = $id; + return $this; + } - /** - * Sets the parent Model - * - * @return $this - * @param \Kirby\Cms\Site|\Kirby\Cms\Page|\Kirby\Cms\File|\Kirby\Cms\User|null $parent - */ - protected function setParent(Model $parent = null) - { - $this->parent = $parent; - return $this; - } + /** + * Sets the parent Model + * + * @return $this + * @param \Kirby\Cms\Site|\Kirby\Cms\Page|\Kirby\Cms\File|\Kirby\Cms\User|null $parent + */ + protected function setParent(Model $parent = null) + { + $this->parent = $parent; + return $this; + } - /** - * Sets the parent Structure collection - * - * @param \Kirby\Cms\Structure|null $structure - * @return $this - */ - protected function setStructure(Structure $structure = null) - { - $this->structure = $structure; - return $this; - } + /** + * Sets the parent Structure collection + * + * @param \Kirby\Cms\Structure|null $structure + * @return $this + */ + protected function setStructure(Structure $structure = null) + { + $this->structure = $structure; + return $this; + } - /** - * Returns the parent Structure collection as siblings - * - * @return \Kirby\Cms\Structure - */ - protected function siblingsCollection() - { - return $this->structure; - } + /** + * Returns the parent Structure collection as siblings + * + * @return \Kirby\Cms\Structure + */ + protected function siblingsCollection() + { + return $this->structure; + } - /** - * Converts all fields in the object to a - * plain associative array. The id is - * injected into the array afterwards - * to make sure it's always present and - * not overloaded in the content. - * - * @return array - */ - public function toArray(): array - { - $array = $this->content()->toArray(); - $array['id'] = $this->id(); + /** + * Converts all fields in the object to a + * plain associative array. The id is + * injected into the array afterwards + * to make sure it's always present and + * not overloaded in the content. + * + * @return array + */ + public function toArray(): array + { + $array = $this->content()->toArray(); + $array['id'] = $this->id(); - ksort($array); + ksort($array); - return $array; - } + return $array; + } } diff --git a/kirby/src/Cms/System.php b/kirby/src/Cms/System.php index d182a5e..bf19418 100644 --- a/kirby/src/Cms/System.php +++ b/kirby/src/Cms/System.php @@ -30,564 +30,614 @@ use Throwable; */ class System { - /** - * @var \Kirby\Cms\App - */ - protected $app; - - /** - * @param \Kirby\Cms\App $app - */ - public function __construct(App $app) - { - $this->app = $app; - - // try to create all folders that could be missing - $this->init(); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } - - /** - * Check for a writable accounts folder - * - * @return bool - */ - public function accounts(): bool - { - return is_writable($this->app->root('accounts')); - } - - /** - * Check for a writable content folder - * - * @return bool - */ - public function content(): bool - { - return is_writable($this->app->root('content')); - } - - /** - * Check for an existing curl extension - * - * @return bool - */ - public function curl(): bool - { - return extension_loaded('curl'); - } - - /** - * Returns the app's human-readable - * index URL without scheme - * - * @return string - */ - public function indexUrl(): string - { - return $this->app->url('index', true)->setScheme(null)->setSlash(false)->toString(); - } - - /** - * Create the most important folders - * if they don't exist yet - * - * @return void - * @throws \Kirby\Exception\PermissionException - */ - public function init() - { - // init /site/accounts - try { - Dir::make($this->app->root('accounts')); - } catch (Throwable $e) { - throw new PermissionException('The accounts directory could not be created'); - } - - // init /site/sessions - try { - Dir::make($this->app->root('sessions')); - } catch (Throwable $e) { - throw new PermissionException('The sessions directory could not be created'); - } - - // init /content - try { - Dir::make($this->app->root('content')); - } catch (Throwable $e) { - throw new PermissionException('The content directory could not be created'); - } - - // init /media - try { - Dir::make($this->app->root('media')); - } catch (Throwable $e) { - throw new PermissionException('The media directory could not be created'); - } - } - - /** - * Check if the panel is installable. - * On a public server the panel.install - * option must be explicitly set to true - * to get the installer up and running. - * - * @return bool - */ - public function isInstallable(): bool - { - return $this->isLocal() === true || $this->app->option('panel.install', false) === true; - } - - /** - * Check if Kirby is already installed - * - * @return bool - */ - public function isInstalled(): bool - { - return $this->app->users()->count() > 0; - } - - /** - * Check if this is a local installation - * - * @return bool - */ - public function isLocal(): bool - { - $server = $this->app->server(); - $visitor = $this->app->visitor(); - $host = $server->host(); - - if ($host === 'localhost') { - return true; - } - - if (Str::endsWith($host, '.local') === true) { - return true; - } - - if (Str::endsWith($host, '.test') === true) { - return true; - } - - if (in_array($visitor->ip(), ['::1', '127.0.0.1']) === true) { - // ensure that there is no reverse proxy in between - - if ( - isset($_SERVER['HTTP_X_FORWARDED_FOR']) === true && - in_array($_SERVER['HTTP_X_FORWARDED_FOR'], ['::1', '127.0.0.1']) === false - ) { - return false; - } - - if ( - isset($_SERVER['HTTP_CLIENT_IP']) === true && - in_array($_SERVER['HTTP_CLIENT_IP'], ['::1', '127.0.0.1']) === false - ) { - return false; - } - - // no reverse proxy or the real client also comes from localhost - return true; - } - - return false; - } - - /** - * Check if all tests pass - * - * @return bool - */ - public function isOk(): bool - { - return in_array(false, array_values($this->status()), true) === false; - } - - /** - * Loads the license file and returns - * the license information if available - * - * @return string|bool License key or `false` if the current user has - * permissions for access.settings, otherwise just a - * boolean that tells whether a valid license is active - */ - public function license() - { - try { - $license = Json::read($this->app->root('license')); - } catch (Throwable $e) { - return false; - } - - // check for all required fields for the validation - if (isset( - $license['license'], - $license['order'], - $license['date'], - $license['email'], - $license['domain'], - $license['signature'] - ) !== true) { - return false; - } - - // build the license verification data - $data = [ - 'license' => $license['license'], - 'order' => $license['order'], - 'email' => hash('sha256', $license['email'] . 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX'), - 'domain' => $license['domain'], - 'date' => $license['date'] - ]; - - - // get the public key - $pubKey = F::read($this->app->root('kirby') . '/kirby.pub'); - - // verify the license signature - if (openssl_verify(json_encode($data), hex2bin($license['signature']), $pubKey, 'RSA-SHA256') !== 1) { - return false; - } - - // verify the URL - if ($this->licenseUrl() !== $this->licenseUrl($license['domain'])) { - return false; - } - - // only return the actual license key if the - // current user has appropriate permissions - $user = $this->app->user(); - if ($user && $user->isAdmin() === true) { - return $license['license']; - } else { - return true; - } - } - - /** - * Normalizes the app's index URL for - * licensing purposes - * - * @param string|null $url Input URL, by default the app's index URL - * @return string Normalized URL - */ - protected function licenseUrl(string $url = null): string - { - if ($url === null) { - $url = $this->indexUrl(); - } - - // remove common "testing" subdomains as well as www. - // to ensure that installations of the same site have - // the same license URL; only for installations at /, - // subdirectory installations are difficult to normalize - if (Str::contains($url, '/') === false) { - if (Str::startsWith($url, 'www.')) { - return substr($url, 4); - } - - if (Str::startsWith($url, 'dev.')) { - return substr($url, 4); - } - - if (Str::startsWith($url, 'test.')) { - return substr($url, 5); - } - - if (Str::startsWith($url, 'staging.')) { - return substr($url, 8); - } - } - - return $url; - } - - /** - * Returns the configured UI modes for the login form - * with their respective options - * - * @return array - * - * @throws \Kirby\Exception\InvalidArgumentException If the configuration is invalid - * (only in debug mode) - */ - public function loginMethods(): array - { - $default = ['password' => []]; - $methods = A::wrap($this->app->option('auth.methods', $default)); - - // normalize the syntax variants - $normalized = []; - $uses2fa = false; - foreach ($methods as $key => $value) { - if (is_int($key) === true) { - // ['password'] - $normalized[$value] = []; - } elseif ($value === true) { - // ['password' => true] - $normalized[$key] = []; - } else { - // ['password' => [...]] - $normalized[$key] = $value; - - if (isset($value['2fa']) === true && $value['2fa'] === true) { - $uses2fa = true; - } - } - } - - // 2FA must not be circumvented by code-based modes - foreach (['code', 'password-reset'] as $method) { - if ($uses2fa === true && isset($normalized[$method]) === true) { - unset($normalized[$method]); - - if ($this->app->option('debug') === true) { - $message = 'The "' . $method . '" login method cannot be enabled when 2FA is required'; - throw new InvalidArgumentException($message); - } - } - } - - // only one code-based mode can be active at once - if ( - isset($normalized['code']) === true && - isset($normalized['password-reset']) === true - ) { - unset($normalized['code']); - - if ($this->app->option('debug') === true) { - $message = 'The "code" and "password-reset" login methods cannot be enabled together'; - throw new InvalidArgumentException($message); - } - } - - return $normalized; - } - - /** - * Check for an existing mbstring extension - * - * @return bool - */ - public function mbString(): bool - { - return extension_loaded('mbstring'); - } - - /** - * Check for a writable media folder - * - * @return bool - */ - public function media(): bool - { - return is_writable($this->app->root('media')); - } - - /** - * Check for a valid PHP version - * - * @return bool - */ - public function php(): bool - { - return - version_compare(PHP_VERSION, '7.4.0', '>=') === true && - version_compare(PHP_VERSION, '8.2.0', '<') === true; - } - - /** - * Returns a sorted collection of all - * installed plugins - * - * @return \Kirby\Cms\Collection - */ - public function plugins() - { - return (new Collection(App::instance()->plugins()))->sortBy('name', 'asc'); - } - - /** - * Validates the license key - * and adds it to the .license file in the config - * folder if possible. - * - * @param string|null $license - * @param string|null $email - * @return bool - * @throws \Kirby\Exception\Exception - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function register(string $license = null, string $email = null): bool - { - if (Str::startsWith($license, 'K3-PRO-') === false) { - throw new InvalidArgumentException([ - 'key' => 'license.format' - ]); - } - - if (V::email($email) === false) { - throw new InvalidArgumentException([ - 'key' => 'license.email' - ]); - } - - // @codeCoverageIgnoreStart - $response = Remote::get('https://licenses.getkirby.com/register', [ - 'data' => [ - 'license' => $license, - 'email' => Str::lower(trim($email)), - 'domain' => $this->indexUrl() - ] - ]); - - if ($response->code() !== 200) { - throw new Exception($response->content()); - } - - // decode the response - $json = Json::decode($response->content()); - - // replace the email with the plaintext version - $json['email'] = $email; - - // where to store the license file - $file = $this->app->root('license'); - - // save the license information - Json::write($file, $json); - - if ($this->license() === false) { - throw new InvalidArgumentException([ - 'key' => 'license.verification' - ]); - } - // @codeCoverageIgnoreEnd - - return true; - } - - /** - * Check for a valid server environment - * - * @return bool - */ - public function server(): bool - { - return $this->serverSoftware() !== null; - } - - /** - * Returns the detected server software - * - * @return string|null - */ - public function serverSoftware(): ?string - { - if ($servers = $this->app->option('servers')) { - $servers = A::wrap($servers); - } else { - $servers = [ - 'apache', - 'caddy', - 'litespeed', - 'nginx', - 'php' - ]; - } - - $software = $_SERVER['SERVER_SOFTWARE'] ?? ''; - - preg_match('!(' . implode('|', $servers) . ')!i', $software, $matches); - - return $matches[0] ?? null; - } - - /** - * Check for a writable sessions folder - * - * @return bool - */ - public function sessions(): bool - { - return is_writable($this->app->root('sessions')); - } - - /** - * Get an status array of all checks - * - * @return array - */ - public function status(): array - { - return [ - 'accounts' => $this->accounts(), - 'content' => $this->content(), - 'curl' => $this->curl(), - 'sessions' => $this->sessions(), - 'mbstring' => $this->mbstring(), - 'media' => $this->media(), - 'php' => $this->php(), - 'server' => $this->server(), - ]; - } - - /** - * Returns the site's title as defined in the - * content file or `site.yml` blueprint - * @since 3.6.0 - * - * @return string - */ - public function title(): string - { - $site = $this->app->site(); - - if ($site->title()->isNotEmpty()) { - return $site->title()->value(); - } - - return $site->blueprint()->title(); - } - - /** - * @return array - */ - public function toArray(): array - { - return $this->status(); - } - - /** - * Upgrade to the new folder separator - * - * @param string $root - * @return void - */ - public static function upgradeContent(string $root) - { - $index = Dir::read($root); - - foreach ($index as $dir) { - $oldRoot = $root . '/' . $dir; - $newRoot = preg_replace('!\/([0-9]+)\-!', '/$1_', $oldRoot); - - if (is_dir($oldRoot) === true) { - Dir::move($oldRoot, $newRoot); - static::upgradeContent($newRoot); - } - } - } + /** + * @var \Kirby\Cms\App + */ + protected $app; + + /** + * @param \Kirby\Cms\App $app + */ + public function __construct(App $app) + { + $this->app = $app; + + // try to create all folders that could be missing + $this->init(); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } + + /** + * Check for a writable accounts folder + * + * @return bool + */ + public function accounts(): bool + { + return is_writable($this->app->root('accounts')); + } + + /** + * Check for a writable content folder + * + * @return bool + */ + public function content(): bool + { + return is_writable($this->app->root('content')); + } + + /** + * Check for an existing curl extension + * + * @return bool + */ + public function curl(): bool + { + return extension_loaded('curl'); + } + + /** + * Returns the URL to the file within a system folder + * if the file is located in the document + * root. Otherwise it will return null. + * + * @param string $folder 'git', 'content', 'site', 'kirby' + * @return string|null + */ + public function exposedFileUrl(string $folder): ?string + { + if (!$url = $this->folderUrl($folder)) { + return null; + } + + switch ($folder) { + case 'content': + return $url . '/' . basename($this->app->site()->contentFile()); + case 'git': + return $url . '/config'; + case 'kirby': + return $url . '/composer.json'; + case 'site': + $root = $this->app->root('site'); + $files = glob($root . '/blueprints/*.yml'); + + if (empty($files) === true) { + $files = glob($root . '/templates/*.*'); + } + + if (empty($files) === true) { + $files = glob($root . '/snippets/*.*'); + } + + if (empty($files) === true || empty($files[0]) === true) { + return $url; + } + + $file = $files[0]; + $file = basename(dirname($file)) . '/' . basename($file); + + return $url . '/' . $file; + default: + return null; + } + } + + /** + * Returns the URL to a system folder + * if the folder is located in the document + * root. Otherwise it will return null. + * + * @param string $folder 'git', 'content', 'site', 'kirby' + * @return string|null + */ + public function folderUrl(string $folder): ?string + { + $index = $this->app->root('index'); + + if ($folder === 'git') { + $root = $index . '/.git'; + } else { + $root = $this->app->root($folder); + } + + if ($root === null || is_dir($root) === false || is_dir($index) === false) { + return null; + } + + $root = realpath($root); + $index = realpath($index); + + // windows + $root = str_replace('\\', '/', $root); + $index = str_replace('\\', '/', $index); + + // the folder is not within the document root? + if (Str::startsWith($root, $index) === false) { + return null; + } + + // get the path after the document root + $path = trim(Str::after($root, $index), '/'); + + // build the absolute URL to the folder + return Url::to($path); + } + + /** + * Returns the app's human-readable + * index URL without scheme + * + * @return string + */ + public function indexUrl(): string + { + return $this->app->url('index', true)->setScheme(null)->setSlash(false)->toString(); + } + + /** + * Create the most important folders + * if they don't exist yet + * + * @return void + * @throws \Kirby\Exception\PermissionException + */ + public function init() + { + // init /site/accounts + try { + Dir::make($this->app->root('accounts')); + } catch (Throwable $e) { + throw new PermissionException('The accounts directory could not be created'); + } + + // init /site/sessions + try { + Dir::make($this->app->root('sessions')); + } catch (Throwable $e) { + throw new PermissionException('The sessions directory could not be created'); + } + + // init /content + try { + Dir::make($this->app->root('content')); + } catch (Throwable $e) { + throw new PermissionException('The content directory could not be created'); + } + + // init /media + try { + Dir::make($this->app->root('media')); + } catch (Throwable $e) { + throw new PermissionException('The media directory could not be created'); + } + } + + /** + * Check if the panel is installable. + * On a public server the panel.install + * option must be explicitly set to true + * to get the installer up and running. + * + * @return bool + */ + public function isInstallable(): bool + { + return $this->isLocal() === true || $this->app->option('panel.install', false) === true; + } + + /** + * Check if Kirby is already installed + * + * @return bool + */ + public function isInstalled(): bool + { + return $this->app->users()->count() > 0; + } + + /** + * Check if this is a local installation + * + * @return bool + */ + public function isLocal(): bool + { + return $this->app->environment()->isLocal(); + } + + /** + * Check if all tests pass + * + * @return bool + */ + public function isOk(): bool + { + return in_array(false, array_values($this->status()), true) === false; + } + + /** + * Loads the license file and returns + * the license information if available + * + * @return string|bool License key or `false` if the current user has + * permissions for access.settings, otherwise just a + * boolean that tells whether a valid license is active + */ + public function license() + { + try { + $license = Json::read($this->app->root('license')); + } catch (Throwable $e) { + return false; + } + + // check for all required fields for the validation + if (isset( + $license['license'], + $license['order'], + $license['date'], + $license['email'], + $license['domain'], + $license['signature'] + ) !== true) { + return false; + } + + // build the license verification data + $data = [ + 'license' => $license['license'], + 'order' => $license['order'], + 'email' => hash('sha256', $license['email'] . 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX'), + 'domain' => $license['domain'], + 'date' => $license['date'] + ]; + + + // get the public key + $pubKey = F::read($this->app->root('kirby') . '/kirby.pub'); + + // verify the license signature + if (openssl_verify(json_encode($data), hex2bin($license['signature']), $pubKey, 'RSA-SHA256') !== 1) { + return false; + } + + // verify the URL + if ($this->licenseUrl() !== $this->licenseUrl($license['domain'])) { + return false; + } + + // only return the actual license key if the + // current user has appropriate permissions + $user = $this->app->user(); + if ($user && $user->isAdmin() === true) { + return $license['license']; + } else { + return true; + } + } + + /** + * Normalizes the app's index URL for + * licensing purposes + * + * @param string|null $url Input URL, by default the app's index URL + * @return string Normalized URL + */ + protected function licenseUrl(string $url = null): string + { + if ($url === null) { + $url = $this->indexUrl(); + } + + // remove common "testing" subdomains as well as www. + // to ensure that installations of the same site have + // the same license URL; only for installations at /, + // subdirectory installations are difficult to normalize + if (Str::contains($url, '/') === false) { + if (Str::startsWith($url, 'www.')) { + return substr($url, 4); + } + + if (Str::startsWith($url, 'dev.')) { + return substr($url, 4); + } + + if (Str::startsWith($url, 'test.')) { + return substr($url, 5); + } + + if (Str::startsWith($url, 'staging.')) { + return substr($url, 8); + } + } + + return $url; + } + + /** + * Returns the configured UI modes for the login form + * with their respective options + * + * @return array + * + * @throws \Kirby\Exception\InvalidArgumentException If the configuration is invalid + * (only in debug mode) + */ + public function loginMethods(): array + { + $default = ['password' => []]; + $methods = A::wrap($this->app->option('auth.methods', $default)); + + // normalize the syntax variants + $normalized = []; + $uses2fa = false; + foreach ($methods as $key => $value) { + if (is_int($key) === true) { + // ['password'] + $normalized[$value] = []; + } elseif ($value === true) { + // ['password' => true] + $normalized[$key] = []; + } else { + // ['password' => [...]] + $normalized[$key] = $value; + + if (isset($value['2fa']) === true && $value['2fa'] === true) { + $uses2fa = true; + } + } + } + + // 2FA must not be circumvented by code-based modes + foreach (['code', 'password-reset'] as $method) { + if ($uses2fa === true && isset($normalized[$method]) === true) { + unset($normalized[$method]); + + if ($this->app->option('debug') === true) { + $message = 'The "' . $method . '" login method cannot be enabled when 2FA is required'; + throw new InvalidArgumentException($message); + } + } + } + + // only one code-based mode can be active at once + if ( + isset($normalized['code']) === true && + isset($normalized['password-reset']) === true + ) { + unset($normalized['code']); + + if ($this->app->option('debug') === true) { + $message = 'The "code" and "password-reset" login methods cannot be enabled together'; + throw new InvalidArgumentException($message); + } + } + + return $normalized; + } + + /** + * Check for an existing mbstring extension + * + * @return bool + */ + public function mbString(): bool + { + return extension_loaded('mbstring'); + } + + /** + * Check for a writable media folder + * + * @return bool + */ + public function media(): bool + { + return is_writable($this->app->root('media')); + } + + /** + * Check for a valid PHP version + * + * @return bool + */ + public function php(): bool + { + return + version_compare(PHP_VERSION, '7.4.0', '>=') === true && + version_compare(PHP_VERSION, '8.2.0', '<') === true; + } + + /** + * Returns a sorted collection of all + * installed plugins + * + * @return \Kirby\Cms\Collection + */ + public function plugins() + { + return (new Collection(App::instance()->plugins()))->sortBy('name', 'asc'); + } + + /** + * Validates the license key + * and adds it to the .license file in the config + * folder if possible. + * + * @param string|null $license + * @param string|null $email + * @return bool + * @throws \Kirby\Exception\Exception + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function register(string $license = null, string $email = null): bool + { + if (Str::startsWith($license, 'K3-PRO-') === false) { + throw new InvalidArgumentException([ + 'key' => 'license.format' + ]); + } + + if (V::email($email) === false) { + throw new InvalidArgumentException([ + 'key' => 'license.email' + ]); + } + + // @codeCoverageIgnoreStart + $response = Remote::get('https://hub.getkirby.com/register', [ + 'data' => [ + 'license' => $license, + 'email' => Str::lower(trim($email)), + 'domain' => $this->indexUrl() + ] + ]); + + if ($response->code() !== 200) { + throw new Exception($response->content()); + } + + // decode the response + $json = Json::decode($response->content()); + + // replace the email with the plaintext version + $json['email'] = $email; + + // where to store the license file + $file = $this->app->root('license'); + + // save the license information + Json::write($file, $json); + + if ($this->license() === false) { + throw new InvalidArgumentException([ + 'key' => 'license.verification' + ]); + } + // @codeCoverageIgnoreEnd + + return true; + } + + /** + * Check for a valid server environment + * + * @return bool + */ + public function server(): bool + { + return $this->serverSoftware() !== null; + } + + /** + * Returns the detected server software + * + * @return string|null + */ + public function serverSoftware(): ?string + { + if ($servers = $this->app->option('servers')) { + $servers = A::wrap($servers); + } else { + $servers = [ + 'apache', + 'caddy', + 'litespeed', + 'nginx', + 'php' + ]; + } + + $software = $this->app->environment()->get('SERVER_SOFTWARE', ''); + + preg_match('!(' . implode('|', $servers) . ')!i', $software, $matches); + + return $matches[0] ?? null; + } + + /** + * Check for a writable sessions folder + * + * @return bool + */ + public function sessions(): bool + { + return is_writable($this->app->root('sessions')); + } + + /** + * Get an status array of all checks + * + * @return array + */ + public function status(): array + { + return [ + 'accounts' => $this->accounts(), + 'content' => $this->content(), + 'curl' => $this->curl(), + 'sessions' => $this->sessions(), + 'mbstring' => $this->mbstring(), + 'media' => $this->media(), + 'php' => $this->php(), + 'server' => $this->server(), + ]; + } + + /** + * Returns the site's title as defined in the + * content file or `site.yml` blueprint + * @since 3.6.0 + * + * @return string + */ + public function title(): string + { + $site = $this->app->site(); + + if ($site->title()->isNotEmpty()) { + return $site->title()->value(); + } + + return $site->blueprint()->title(); + } + + /** + * @return array + */ + public function toArray(): array + { + return $this->status(); + } + + /** + * Upgrade to the new folder separator + * + * @param string $root + * @return void + */ + public static function upgradeContent(string $root) + { + $index = Dir::read($root); + + foreach ($index as $dir) { + $oldRoot = $root . '/' . $dir; + $newRoot = preg_replace('!\/([0-9]+)\-!', '/$1_', $oldRoot); + + if (is_dir($oldRoot) === true) { + Dir::move($oldRoot, $newRoot); + static::upgradeContent($newRoot); + } + } + } } diff --git a/kirby/src/Cms/Template.php b/kirby/src/Cms/Template.php index 8241442..e3e7e07 100644 --- a/kirby/src/Cms/Template.php +++ b/kirby/src/Cms/Template.php @@ -18,188 +18,188 @@ use Kirby\Toolkit\Tpl; */ class Template { - /** - * Global template data - * - * @var array - */ - public static $data = []; + /** + * Global template data + * + * @var array + */ + public static $data = []; - /** - * The name of the template - * - * @var string - */ - protected $name; + /** + * The name of the template + * + * @var string + */ + protected $name; - /** - * Template type (html, json, etc.) - * - * @var string - */ - protected $type; + /** + * Template type (html, json, etc.) + * + * @var string + */ + protected $type; - /** - * Default template type if no specific type is set - * - * @var string - */ - protected $defaultType; + /** + * Default template type if no specific type is set + * + * @var string + */ + protected $defaultType; - /** - * Creates a new template object - * - * @param string $name - * @param string $type - * @param string $defaultType - */ - public function __construct(string $name, string $type = 'html', string $defaultType = 'html') - { - $this->name = strtolower($name); - $this->type = $type; - $this->defaultType = $defaultType; - } + /** + * Creates a new template object + * + * @param string $name + * @param string $type + * @param string $defaultType + */ + public function __construct(string $name, string $type = 'html', string $defaultType = 'html') + { + $this->name = strtolower($name); + $this->type = $type; + $this->defaultType = $defaultType; + } - /** - * Converts the object to a simple string - * This is used in template filters for example - * - * @return string - */ - public function __toString(): string - { - return $this->name; - } + /** + * Converts the object to a simple string + * This is used in template filters for example + * + * @return string + */ + public function __toString(): string + { + return $this->name; + } - /** - * Checks if the template exists - * - * @return bool - */ - public function exists(): bool - { - if ($file = $this->file()) { - return file_exists($file); - } + /** + * Checks if the template exists + * + * @return bool + */ + public function exists(): bool + { + if ($file = $this->file()) { + return file_exists($file); + } - return false; - } + return false; + } - /** - * Returns the expected template file extension - * - * @return string - */ - public function extension(): string - { - return 'php'; - } + /** + * Returns the expected template file extension + * + * @return string + */ + public function extension(): string + { + return 'php'; + } - /** - * Returns the default template type - * - * @return string - */ - public function defaultType(): string - { - return $this->defaultType; - } + /** + * Returns the default template type + * + * @return string + */ + public function defaultType(): string + { + return $this->defaultType; + } - /** - * Returns the place where templates are located - * in the site folder and and can be found in extensions - * - * @return string - */ - public function store(): string - { - return 'templates'; - } + /** + * Returns the place where templates are located + * in the site folder and and can be found in extensions + * + * @return string + */ + public function store(): string + { + return 'templates'; + } - /** - * Detects the location of the template file - * if it exists. - * - * @return string|null - */ - public function file(): ?string - { - if ($this->hasDefaultType() === true) { - try { - // Try the default template in the default template directory. - return F::realpath($this->root() . '/' . $this->name() . '.' . $this->extension(), $this->root()); - } catch (Exception $e) { - // ignore errors, continue searching - } + /** + * Detects the location of the template file + * if it exists. + * + * @return string|null + */ + public function file(): ?string + { + if ($this->hasDefaultType() === true) { + try { + // Try the default template in the default template directory. + return F::realpath($this->root() . '/' . $this->name() . '.' . $this->extension(), $this->root()); + } catch (Exception $e) { + // ignore errors, continue searching + } - // Look for the default template provided by an extension. - $path = App::instance()->extension($this->store(), $this->name()); + // Look for the default template provided by an extension. + $path = App::instance()->extension($this->store(), $this->name()); - if ($path !== null) { - return $path; - } - } + if ($path !== null) { + return $path; + } + } - $name = $this->name() . '.' . $this->type(); + $name = $this->name() . '.' . $this->type(); - try { - // Try the template with type extension in the default template directory. - return F::realpath($this->root() . '/' . $name . '.' . $this->extension(), $this->root()); - } catch (Exception $e) { - // Look for the template with type extension provided by an extension. - // This might be null if the template does not exist. - return App::instance()->extension($this->store(), $name); - } - } + try { + // Try the template with type extension in the default template directory. + return F::realpath($this->root() . '/' . $name . '.' . $this->extension(), $this->root()); + } catch (Exception $e) { + // Look for the template with type extension provided by an extension. + // This might be null if the template does not exist. + return App::instance()->extension($this->store(), $name); + } + } - /** - * Returns the template name - * - * @return string - */ - public function name(): string - { - return $this->name; - } + /** + * Returns the template name + * + * @return string + */ + public function name(): string + { + return $this->name; + } - /** - * @param array $data - * @return string - */ - public function render(array $data = []): string - { - return Tpl::load($this->file(), $data); - } + /** + * @param array $data + * @return string + */ + public function render(array $data = []): string + { + return Tpl::load($this->file(), $data); + } - /** - * Returns the root to the templates directory - * - * @return string - */ - public function root(): string - { - return App::instance()->root($this->store()); - } + /** + * Returns the root to the templates directory + * + * @return string + */ + public function root(): string + { + return App::instance()->root($this->store()); + } - /** - * Returns the template type - * - * @return string - */ - public function type(): string - { - return $this->type; - } + /** + * Returns the template type + * + * @return string + */ + public function type(): string + { + return $this->type; + } - /** - * Checks if the template uses the default type - * - * @return bool - */ - public function hasDefaultType(): bool - { - $type = $this->type(); + /** + * Checks if the template uses the default type + * + * @return bool + */ + public function hasDefaultType(): bool + { + $type = $this->type(); - return $type === null || $type === $this->defaultType(); - } + return $type === null || $type === $this->defaultType(); + } } diff --git a/kirby/src/Cms/Translation.php b/kirby/src/Cms/Translation.php index 1a0e15c..b297ba4 100644 --- a/kirby/src/Cms/Translation.php +++ b/kirby/src/Cms/Translation.php @@ -18,178 +18,178 @@ use Kirby\Toolkit\Str; */ class Translation { - /** - * @var string - */ - protected $code; + /** + * @var string + */ + protected $code; - /** - * @var array - */ - protected $data = []; + /** + * @var array + */ + protected $data = []; - /** - * @param string $code - * @param array $data - */ - public function __construct(string $code, array $data) - { - $this->code = $code; - $this->data = $data; - } + /** + * @param string $code + * @param array $data + */ + public function __construct(string $code, array $data) + { + $this->code = $code; + $this->data = $data; + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * Returns the translation author - * - * @return string - */ - public function author(): string - { - return $this->get('translation.author', 'Kirby'); - } + /** + * Returns the translation author + * + * @return string + */ + public function author(): string + { + return $this->get('translation.author', 'Kirby'); + } - /** - * Returns the official translation code - * - * @return string - */ - public function code(): string - { - return $this->code; - } + /** + * Returns the official translation code + * + * @return string + */ + public function code(): string + { + return $this->code; + } - /** - * Returns an array with all - * translation strings - * - * @return array - */ - public function data(): array - { - return $this->data; - } + /** + * Returns an array with all + * translation strings + * + * @return array + */ + public function data(): array + { + return $this->data; + } - /** - * Returns the translation data and merges - * it with the data from the default translation - * - * @return array - */ - public function dataWithFallback(): array - { - if ($this->code === 'en') { - return $this->data; - } + /** + * Returns the translation data and merges + * it with the data from the default translation + * + * @return array + */ + public function dataWithFallback(): array + { + if ($this->code === 'en') { + return $this->data; + } - // get the fallback array - $fallback = App::instance()->translation('en')->data(); + // get the fallback array + $fallback = App::instance()->translation('en')->data(); - return array_merge($fallback, $this->data); - } + return array_merge($fallback, $this->data); + } - /** - * Returns the writing direction - * (ltr or rtl) - * - * @return string - */ - public function direction(): string - { - return $this->get('translation.direction', 'ltr'); - } + /** + * Returns the writing direction + * (ltr or rtl) + * + * @return string + */ + public function direction(): string + { + return $this->get('translation.direction', 'ltr'); + } - /** - * Returns a single translation - * string by key - * - * @param string $key - * @param string|null $default - * @return string|null - */ - public function get(string $key, string $default = null): ?string - { - return $this->data[$key] ?? $default; - } + /** + * Returns a single translation + * string by key + * + * @param string $key + * @param string|null $default + * @return string|null + */ + public function get(string $key, string $default = null): ?string + { + return $this->data[$key] ?? $default; + } - /** - * Returns the translation id, - * which is also the code - * - * @return string - */ - public function id(): string - { - return $this->code; - } + /** + * Returns the translation id, + * which is also the code + * + * @return string + */ + public function id(): string + { + return $this->code; + } - /** - * Loads the translation from the - * json file in Kirby's translations folder - * - * @param string $code - * @param string $root - * @param array $inject - * @return static - */ - public static function load(string $code, string $root, array $inject = []) - { - try { - $data = array_merge(Data::read($root), $inject); - } catch (Exception $e) { - $data = []; - } + /** + * Loads the translation from the + * json file in Kirby's translations folder + * + * @param string $code + * @param string $root + * @param array $inject + * @return static + */ + public static function load(string $code, string $root, array $inject = []) + { + try { + $data = array_merge(Data::read($root), $inject); + } catch (Exception $e) { + $data = []; + } - return new static($code, $data); - } + return new static($code, $data); + } - /** - * Returns the PHP locale of the translation - * - * @return string - */ - public function locale(): string - { - $default = $this->code; - if (Str::contains($default, '_') !== true) { - $default .= '_' . strtoupper($this->code); - } + /** + * Returns the PHP locale of the translation + * + * @return string + */ + public function locale(): string + { + $default = $this->code; + if (Str::contains($default, '_') !== true) { + $default .= '_' . strtoupper($this->code); + } - return $this->get('translation.locale', $default); - } + return $this->get('translation.locale', $default); + } - /** - * Returns the human-readable translation name. - * - * @return string - */ - public function name(): string - { - return $this->get('translation.name', $this->code); - } + /** + * Returns the human-readable translation name. + * + * @return string + */ + public function name(): string + { + return $this->get('translation.name', $this->code); + } - /** - * Converts the most important - * properties to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'code' => $this->code(), - 'data' => $this->data(), - 'name' => $this->name(), - 'author' => $this->author(), - ]; - } + /** + * Converts the most important + * properties to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'code' => $this->code(), + 'data' => $this->data(), + 'name' => $this->name(), + 'author' => $this->author(), + ]; + } } diff --git a/kirby/src/Cms/Translations.php b/kirby/src/Cms/Translations.php index 0512997..c45dd40 100644 --- a/kirby/src/Cms/Translations.php +++ b/kirby/src/Cms/Translations.php @@ -19,60 +19,60 @@ use Kirby\Filesystem\F; */ class Translations extends Collection { - /** - * @param string $code - * @return void - */ - public function start(string $code): void - { - F::move($this->parent->contentFile('', true), $this->parent->contentFile($code, true)); - } + /** + * @param string $code + * @return void + */ + public function start(string $code): void + { + F::move($this->parent->contentFile('', true), $this->parent->contentFile($code, true)); + } - /** - * @param string $code - * @return void - */ - public function stop(string $code): void - { - F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true)); - } + /** + * @param string $code + * @return void + */ + public function stop(string $code): void + { + F::move($this->parent->contentFile($code, true), $this->parent->contentFile('', true)); + } - /** - * @param array $translations - * @return static - */ - public static function factory(array $translations) - { - $collection = new static(); + /** + * @param array $translations + * @return static + */ + public static function factory(array $translations) + { + $collection = new static(); - foreach ($translations as $code => $props) { - $translation = new Translation($code, $props); - $collection->data[$translation->code()] = $translation; - } + foreach ($translations as $code => $props) { + $translation = new Translation($code, $props); + $collection->data[$translation->code()] = $translation; + } - return $collection; - } + return $collection; + } - /** - * @param string $root - * @param array $inject - * @return static - */ - public static function load(string $root, array $inject = []) - { - $collection = new static(); + /** + * @param string $root + * @param array $inject + * @return static + */ + public static function load(string $root, array $inject = []) + { + $collection = new static(); - foreach (Dir::read($root) as $filename) { - if (F::extension($filename) !== 'json') { - continue; - } + foreach (Dir::read($root) as $filename) { + if (F::extension($filename) !== 'json') { + continue; + } - $locale = F::name($filename); - $translation = Translation::load($locale, $root . '/' . $filename, $inject[$locale] ?? []); + $locale = F::name($filename); + $translation = Translation::load($locale, $root . '/' . $filename, $inject[$locale] ?? []); - $collection->data[$locale] = $translation; - } + $collection->data[$locale] = $translation; + } - return $collection; - } + return $collection; + } } diff --git a/kirby/src/Cms/Url.php b/kirby/src/Cms/Url.php index 4351fbf..9111b09 100644 --- a/kirby/src/Cms/Url.php +++ b/kirby/src/Cms/Url.php @@ -21,46 +21,46 @@ use Kirby\Http\Url as BaseUrl; */ class Url extends BaseUrl { - public static $home = null; + public static $home = null; - /** - * Returns the Url to the homepage - * - * @return string - */ - public static function home(): string - { - return App::instance()->url(); - } + /** + * Returns the Url to the homepage + * + * @return string + */ + public static function home(): string + { + return App::instance()->url(); + } - /** - * Creates an absolute Url to a template asset if it exists. This is used in the `css()` and `js()` helpers - * - * @param string $assetPath - * @param string $extension - * @return string|null - */ - public static function toTemplateAsset(string $assetPath, string $extension) - { - $kirby = App::instance(); - $page = $kirby->site()->page(); - $path = $assetPath . '/' . $page->template() . '.' . $extension; - $file = $kirby->root('assets') . '/' . $path; - $url = $kirby->url('assets') . '/' . $path; + /** + * Creates an absolute Url to a template asset if it exists. This is used in the `css()` and `js()` helpers + * + * @param string $assetPath + * @param string $extension + * @return string|null + */ + public static function toTemplateAsset(string $assetPath, string $extension) + { + $kirby = App::instance(); + $page = $kirby->site()->page(); + $path = $assetPath . '/' . $page->template() . '.' . $extension; + $file = $kirby->root('assets') . '/' . $path; + $url = $kirby->url('assets') . '/' . $path; - return file_exists($file) === true ? $url : null; - } + return file_exists($file) === true ? $url : null; + } - /** - * Smart resolver for internal and external urls - * - * @param string|null $path - * @param array|string|null $options Either an array of options for the Uri class or a language string - * @return string - */ - public static function to(string $path = null, $options = null): string - { - $kirby = App::instance(); - return ($kirby->component('url'))($kirby, $path, $options); - } + /** + * Smart resolver for internal and external urls + * + * @param string|null $path + * @param array|string|null $options Either an array of options for the Uri class or a language string + * @return string + */ + public static function to(string $path = null, $options = null): string + { + $kirby = App::instance(); + return ($kirby->component('url'))($kirby, $path, $options); + } } diff --git a/kirby/src/Cms/User.php b/kirby/src/Cms/User.php index 11ed048..67b138c 100644 --- a/kirby/src/Cms/User.php +++ b/kirby/src/Cms/User.php @@ -22,913 +22,912 @@ use Kirby\Toolkit\Str; */ class User extends ModelWithContent { - use HasFiles; - use HasMethods; - use HasSiblings; - use UserActions; - - public const CLASS_ALIAS = 'user'; - - /** - * @var UserBlueprint - */ - protected $blueprint; - - /** - * @var array - */ - protected $credentials; - - /** - * @var string - */ - protected $email; - - /** - * @var string - */ - protected $hash; - - /** - * @var string - */ - protected $id; - - /** - * @var array|null - */ - protected $inventory; - - /** - * @var string - */ - protected $language; - - /** - * All registered user methods - * - * @var array - */ - public static $methods = []; - - /** - * Registry with all User models - * - * @var array - */ - public static $models = []; - - /** - * @var \Kirby\Cms\Field - */ - protected $name; - - /** - * @var string - */ - protected $password; - - /** - * The user role - * - * @var string - */ - protected $role; - - /** - * Modified getter to also return fields - * from the content - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } - - // user methods - if ($this->hasMethod($method)) { - return $this->callMethod($method, $arguments); - } - - // return site content otherwise - return $this->content()->get($method); - } - - /** - * Creates a new User object - * - * @param array $props - */ - public function __construct(array $props) - { - // TODO: refactor later to avoid redundant prop setting - $this->setProperty('id', $props['id'] ?? $this->createId(), true); - $this->setProperties($props); - } - - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return array_merge($this->toArray(), [ - 'avatar' => $this->avatar(), - 'content' => $this->content(), - 'role' => $this->role() - ]); - } - - /** - * Returns the url to the api endpoint - * - * @internal - * @param bool $relative - * @return string - */ - public function apiUrl(bool $relative = false): string - { - if ($relative === true) { - return 'users/' . $this->id(); - } else { - return $this->kirby()->url('api') . '/users/' . $this->id(); - } - } - - /** - * Returns the File object for the avatar or null - * - * @return \Kirby\Cms\File|null - */ - public function avatar() - { - return $this->files()->template('avatar')->first(); - } - - /** - * Returns the UserBlueprint object - * - * @return \Kirby\Cms\Blueprint - */ - public function blueprint() - { - if (is_a($this->blueprint, 'Kirby\Cms\Blueprint') === true) { - return $this->blueprint; - } - - try { - return $this->blueprint = UserBlueprint::factory('users/' . $this->role(), 'users/default', $this); - } catch (Exception $e) { - return $this->blueprint = new UserBlueprint([ - 'model' => $this, - 'name' => 'default', - 'title' => 'Default', - ]); - } - } - - /** - * Prepares the content for the write method - * - * @internal - * @param array $data - * @param string $languageCode|null Not used so far - * @return array - */ - public function contentFileData(array $data, string $languageCode = null): array - { - // remove stuff that has nothing to do in the text files - unset( - $data['email'], - $data['language'], - $data['name'], - $data['password'], - $data['role'] - ); - - return $data; - } - - /** - * Filename for the content file - * - * @internal - * @return string - */ - public function contentFileName(): string - { - return 'user'; - } - - protected function credentials(): array - { - return $this->credentials ??= $this->readCredentials(); - } - - /** - * Returns the user email address - * - * @return string - */ - public function email(): ?string - { - return $this->email ??= $this->credentials()['email'] ?? null; - } - - /** - * Checks if the user exists - * - * @return bool - */ - public function exists(): bool - { - return is_file($this->contentFile('default')) === true; - } - - /** - * Constructs a User object and also - * takes User models into account. - * - * @internal - * @param mixed $props - * @return static - */ - public static function factory($props) - { - if (empty($props['model']) === false) { - return static::model($props['model'], $props); - } - - return new static($props); - } - - /** - * Hashes the user's password unless it is `null`, - * which will leave it as `null` - * - * @internal - * @param string|null $password - * @return string|null - */ - public static function hashPassword($password): ?string - { - if ($password !== null) { - $password = password_hash($password, PASSWORD_DEFAULT); - } - - return $password; - } - - /** - * Returns the user id - * - * @return string - */ - public function id(): string - { - return $this->id; - } - - /** - * Returns the inventory of files - * children and content files - * - * @return array - */ - public function inventory(): array - { - if ($this->inventory !== null) { - return $this->inventory; - } - - $kirby = $this->kirby(); - - return $this->inventory = Dir::inventory( - $this->root(), - $kirby->contentExtension(), - $kirby->contentIgnore(), - $kirby->multilang() - ); - } - - /** - * Compares the current object with the given user object - * - * @param \Kirby\Cms\User|null $user - * @return bool - */ - public function is(User $user = null): bool - { - if ($user === null) { - return false; - } - - return $this->id() === $user->id(); - } - - /** - * Checks if this user has the admin role - * - * @return bool - */ - public function isAdmin(): bool - { - return $this->role()->id() === 'admin'; - } - - /** - * Checks if the current user is the virtual - * Kirby user - * - * @return bool - */ - public function isKirby(): bool - { - return $this->email() === 'kirby@getkirby.com'; - } - - /** - * Checks if the current user is this user - * - * @return bool - */ - public function isLoggedIn(): bool - { - return $this->is($this->kirby()->user()); - } - - /** - * Checks if the user is the last one - * with the admin role - * - * @return bool - */ - public function isLastAdmin(): bool - { - return $this->role()->isAdmin() === true && - $this->kirby()->users()->filter('role', 'admin')->count() <= 1; - } - - /** - * Checks if the user is the last user - * - * @return bool - */ - public function isLastUser(): bool - { - return $this->kirby()->users()->count() === 1; - } - - /** - * Checks if the current user is the virtual - * Nobody user - * - * @return bool - */ - public function isNobody(): bool - { - return $this->email() === 'nobody@getkirby.com'; - } - - /** - * Returns the user language - * - * @return string - */ - public function language(): string - { - return $this->language ??= $this->credentials()['language'] ?? $this->kirby()->panelLanguage(); - } - - /** - * Logs the user in - * - * @param string $password - * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in - * @return bool - */ - public function login(string $password, $session = null): bool - { - $this->validatePassword($password); - $this->loginPasswordless($session); - - return true; - } - - /** - * Logs the user in without checking the password - * - * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in - * @return void - */ - public function loginPasswordless($session = null): void - { - $kirby = $this->kirby(); - - $session = $this->sessionFromOptions($session); - - $kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]); - - $session->regenerateToken(); // privilege change - $session->data()->set('kirby.userId', $this->id()); - $this->kirby()->auth()->setUser($this); - - $kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]); - } - - /** - * Logs the user out - * - * @param \Kirby\Session\Session|array|null $session Session options or session object to unset the user in - * @return void - */ - public function logout($session = null): void - { - $kirby = $this->kirby(); - $session = $this->sessionFromOptions($session); - - $kirby->trigger('user.logout:before', ['user' => $this, 'session' => $session]); - - // remove the user from the session for future requests - $session->data()->remove('kirby.userId'); - - // clear the cached user object from the app state of the current request - $this->kirby()->auth()->flush(); - - if ($session->data()->get() === []) { - // session is now empty, we might as well destroy it - $session->destroy(); - - $kirby->trigger('user.logout:after', ['user' => $this, 'session' => null]); - } else { - // privilege change - $session->regenerateToken(); - - $kirby->trigger('user.logout:after', ['user' => $this, 'session' => $session]); - } - } - - /** - * Returns the root to the media folder for the user - * - * @internal - * @return string - */ - public function mediaRoot(): string - { - return $this->kirby()->root('media') . '/users/' . $this->id(); - } - - /** - * Returns the media url for the user object - * - * @internal - * @return string - */ - public function mediaUrl(): string - { - return $this->kirby()->url('media') . '/users/' . $this->id(); - } - - /** - * Creates a user model if it has been registered - * - * @internal - * @param string $name - * @param array $props - * @return \Kirby\Cms\User - */ - public static function model(string $name, array $props = []) - { - if ($class = (static::$models[$name] ?? null)) { - $object = new $class($props); - - if (is_a($object, 'Kirby\Cms\User') === true) { - return $object; - } - } - - return new static($props); - } - - /** - * Returns the last modification date of the user - * - * @param string $format - * @param string|null $handler - * @param string|null $languageCode - * @return int|string - */ - public function modified(string $format = 'U', string $handler = null, string $languageCode = null) - { - $modifiedContent = F::modified($this->contentFile($languageCode)); - $modifiedIndex = F::modified($this->root() . '/index.php'); - $modifiedTotal = max([$modifiedContent, $modifiedIndex]); - $handler ??= $this->kirby()->option('date.handler', 'date'); - - return Str::date($modifiedTotal, $format, $handler); - } - - /** - * Returns the user's name - * - * @return \Kirby\Cms\Field - */ - public function name() - { - if (is_string($this->name) === true) { - return new Field($this, 'name', $this->name); - } - - if ($this->name !== null) { - return $this->name; - } - - return $this->name = new Field($this, 'name', $this->credentials()['name'] ?? null); - } - - /** - * Returns the user's name or, - * if empty, the email address - * - * @return \Kirby\Cms\Field - */ - public function nameOrEmail() - { - $name = $this->name(); - return $name->isNotEmpty() ? $name : new Field($this, 'email', $this->email()); - } - - /** - * Create a dummy nobody - * - * @internal - * @return static - */ - public static function nobody() - { - return new static([ - 'email' => 'nobody@getkirby.com', - 'role' => 'nobody' - ]); - } - - /** - * Returns the panel info object - * - * @return \Kirby\Panel\User - */ - public function panel() - { - return new Panel($this); - } - - /** - * Returns the encrypted user password - * - * @return string|null - */ - public function password(): ?string - { - if ($this->password !== null) { - return $this->password; - } - - return $this->password = $this->readPassword(); - } - - /** - * @return \Kirby\Cms\UserPermissions - */ - public function permissions() - { - return new UserPermissions($this); - } - - /** - * Returns the user role - * - * @return \Kirby\Cms\Role - */ - public function role() - { - if (is_a($this->role, 'Kirby\Cms\Role') === true) { - return $this->role; - } - - $roleName = $this->role ?? $this->credentials()['role'] ?? 'visitor'; - - if ($role = $this->kirby()->roles()->find($roleName)) { - return $this->role = $role; - } - - return $this->role = Role::nobody(); - } - - /** - * Returns all available roles - * for this user, that can be selected - * by the authenticated user - * - * @return \Kirby\Cms\Roles - */ - public function roles() - { - $kirby = $this->kirby(); - $roles = $kirby->roles(); - - // a collection with just the one role of the user - $myRole = $roles->filter('id', $this->role()->id()); - - // if there's an authenticated user … - if ($user = $kirby->user()) { - - // admin users can select pretty much any role - if ($user->isAdmin() === true) { - // except if the user is the last admin - if ($this->isLastAdmin() === true) { - // in which case they have to stay admin - return $myRole; - } - - // return all roles for mighty admins - return $roles; - } - } - - // any other user can only keep their role - return $myRole; - } - - /** - * The absolute path to the user directory - * - * @return string - */ - public function root(): string - { - return $this->kirby()->root('accounts') . '/' . $this->id(); - } - - /** - * Returns the UserRules class to - * validate any important action. - * - * @return \Kirby\Cms\UserRules - */ - protected function rules() - { - return new UserRules(); - } - - /** - * Sets the Blueprint object - * - * @param array|null $blueprint - * @return $this - */ - protected function setBlueprint(array $blueprint = null) - { - if ($blueprint !== null) { - $blueprint['model'] = $this; - $this->blueprint = new UserBlueprint($blueprint); - } - - return $this; - } - - /** - * Sets the user email - * - * @param string $email|null - * @return $this - */ - protected function setEmail(string $email = null) - { - if ($email !== null) { - $this->email = Str::lower(trim($email)); - } - return $this; - } - - /** - * Sets the user id - * - * @param string $id|null - * @return $this - */ - protected function setId(string $id = null) - { - $this->id = $id; - return $this; - } - - /** - * Sets the user language - * - * @param string $language|null - * @return $this - */ - protected function setLanguage(string $language = null) - { - $this->language = $language !== null ? trim($language) : null; - return $this; - } - - /** - * Sets the user name - * - * @param string $name|null - * @return $this - */ - protected function setName(string $name = null) - { - $this->name = $name !== null ? trim(strip_tags($name)) : null; - return $this; - } - - /** - * Sets the user's password hash - * - * @param string $password|null - * @return $this - */ - protected function setPassword(string $password = null) - { - $this->password = $password; - return $this; - } - - /** - * Sets the user role - * - * @param string $role|null - * @return $this - */ - protected function setRole(string $role = null) - { - $this->role = $role !== null ? Str::lower(trim($role)) : null; - return $this; - } - - /** - * Converts session options into a session object - * - * @param \Kirby\Session\Session|array $session Session options or session object to unset the user in - * @return \Kirby\Session\Session - */ - protected function sessionFromOptions($session) - { - // use passed session options or session object if set - if (is_array($session) === true) { - $session = $this->kirby()->session($session); - } elseif (is_a($session, 'Kirby\Session\Session') === false) { - $session = $this->kirby()->session(['detect' => true]); - } - - return $session; - } - - /** - * Returns the parent Users collection - * - * @return \Kirby\Cms\Users - */ - protected function siblingsCollection() - { - return $this->kirby()->users(); - } - - /** - * Converts the most important user properties - * to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'avatar' => $this->avatar() ? $this->avatar()->toArray() : null, - 'content' => $this->content()->toArray(), - 'email' => $this->email(), - 'id' => $this->id(), - 'language' => $this->language(), - 'role' => $this->role()->name(), - 'username' => $this->username() - ]; - } - - /** - * String template builder - * - * @param string|null $template - * @param array|null $data - * @param string $fallback Fallback for tokens in the template that cannot be replaced - * @return string - */ - public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string - { - if ($template === null) { - $template = $this->email(); - } - - return parent::toString($template, $data, $fallback, $handler); - } - - /** - * Returns the username - * which is the given name or the email - * as a fallback - * - * @return string|null - */ - public function username(): ?string - { - return $this->name()->or($this->email())->value(); - } - - /** - * Compares the given password with the stored one - * - * @param string $password|null - * @return bool - * - * @throws \Kirby\Exception\NotFoundException If the user has no password - * @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid - * or does not match the user password - */ - public function validatePassword(string $password = null): bool - { - if (empty($this->password()) === true) { - throw new NotFoundException(['key' => 'user.password.undefined']); - } - - if (Str::length($password) < 8) { - throw new InvalidArgumentException(['key' => 'user.password.invalid']); - } - - if (password_verify($password, $this->password()) !== true) { - throw new InvalidArgumentException(['key' => 'user.password.wrong', 'httpCode' => 401]); - } - - return true; - } - - - /** - * Deprecated! - */ - - /** - * Returns the full path without leading slash - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @return string - * @codeCoverageIgnore - */ - public function panelPath(): string - { - return $this->panel()->path(); - } - - /** - * Returns prepared data for the panel user picker - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @param array|null $params - * @return array - * @codeCoverageIgnore - */ - public function panelPickerData(array $params = null): array - { - return $this->panel()->pickerData($params); - } - - /** - * Returns the url to the editing view - * in the panel - * - * @todo Add `deprecated()` helper warning in 3.7.0 - * @todo Remove in 3.8.0 - * - * @internal - * @param bool $relative - * @return string - * @codeCoverageIgnore - */ - public function panelUrl(bool $relative = false): string - { - return $this->panel()->url($relative); - } + use HasFiles; + use HasMethods; + use HasSiblings; + use UserActions; + + public const CLASS_ALIAS = 'user'; + + /** + * @var UserBlueprint + */ + protected $blueprint; + + /** + * @var array + */ + protected $credentials; + + /** + * @var string + */ + protected $email; + + /** + * @var string + */ + protected $hash; + + /** + * @var string + */ + protected $id; + + /** + * @var array|null + */ + protected $inventory; + + /** + * @var string + */ + protected $language; + + /** + * All registered user methods + * + * @var array + */ + public static $methods = []; + + /** + * Registry with all User models + * + * @var array + */ + public static $models = []; + + /** + * @var \Kirby\Cms\Field + */ + protected $name; + + /** + * @var string + */ + protected $password; + + /** + * The user role + * + * @var string + */ + protected $role; + + /** + * Modified getter to also return fields + * from the content + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } + + // user methods + if ($this->hasMethod($method)) { + return $this->callMethod($method, $arguments); + } + + // return site content otherwise + return $this->content()->get($method); + } + + /** + * Creates a new User object + * + * @param array $props + */ + public function __construct(array $props) + { + // TODO: refactor later to avoid redundant prop setting + $this->setProperty('id', $props['id'] ?? $this->createId(), true); + $this->setProperties($props); + } + + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'avatar' => $this->avatar(), + 'content' => $this->content(), + 'role' => $this->role() + ]); + } + + /** + * Returns the url to the api endpoint + * + * @internal + * @param bool $relative + * @return string + */ + public function apiUrl(bool $relative = false): string + { + if ($relative === true) { + return 'users/' . $this->id(); + } else { + return $this->kirby()->url('api') . '/users/' . $this->id(); + } + } + + /** + * Returns the File object for the avatar or null + * + * @return \Kirby\Cms\File|null + */ + public function avatar() + { + return $this->files()->template('avatar')->first(); + } + + /** + * Returns the UserBlueprint object + * + * @return \Kirby\Cms\Blueprint + */ + public function blueprint() + { + if (is_a($this->blueprint, 'Kirby\Cms\Blueprint') === true) { + return $this->blueprint; + } + + try { + return $this->blueprint = UserBlueprint::factory('users/' . $this->role(), 'users/default', $this); + } catch (Exception $e) { + return $this->blueprint = new UserBlueprint([ + 'model' => $this, + 'name' => 'default', + 'title' => 'Default', + ]); + } + } + + /** + * Prepares the content for the write method + * + * @internal + * @param array $data + * @param string $languageCode|null Not used so far + * @return array + */ + public function contentFileData(array $data, string $languageCode = null): array + { + // remove stuff that has nothing to do in the text files + unset( + $data['email'], + $data['language'], + $data['name'], + $data['password'], + $data['role'] + ); + + return $data; + } + + /** + * Filename for the content file + * + * @internal + * @return string + */ + public function contentFileName(): string + { + return 'user'; + } + + protected function credentials(): array + { + return $this->credentials ??= $this->readCredentials(); + } + + /** + * Returns the user email address + * + * @return string + */ + public function email(): ?string + { + return $this->email ??= $this->credentials()['email'] ?? null; + } + + /** + * Checks if the user exists + * + * @return bool + */ + public function exists(): bool + { + return is_file($this->contentFile('default')) === true; + } + + /** + * Constructs a User object and also + * takes User models into account. + * + * @internal + * @param mixed $props + * @return static + */ + public static function factory($props) + { + if (empty($props['model']) === false) { + return static::model($props['model'], $props); + } + + return new static($props); + } + + /** + * Hashes the user's password unless it is `null`, + * which will leave it as `null` + * + * @internal + * @param string|null $password + * @return string|null + */ + public static function hashPassword($password): ?string + { + if ($password !== null) { + $password = password_hash($password, PASSWORD_DEFAULT); + } + + return $password; + } + + /** + * Returns the user id + * + * @return string + */ + public function id(): string + { + return $this->id; + } + + /** + * Returns the inventory of files + * children and content files + * + * @return array + */ + public function inventory(): array + { + if ($this->inventory !== null) { + return $this->inventory; + } + + $kirby = $this->kirby(); + + return $this->inventory = Dir::inventory( + $this->root(), + $kirby->contentExtension(), + $kirby->contentIgnore(), + $kirby->multilang() + ); + } + + /** + * Compares the current object with the given user object + * + * @param \Kirby\Cms\User|null $user + * @return bool + */ + public function is(User $user = null): bool + { + if ($user === null) { + return false; + } + + return $this->id() === $user->id(); + } + + /** + * Checks if this user has the admin role + * + * @return bool + */ + public function isAdmin(): bool + { + return $this->role()->id() === 'admin'; + } + + /** + * Checks if the current user is the virtual + * Kirby user + * + * @return bool + */ + public function isKirby(): bool + { + return $this->email() === 'kirby@getkirby.com'; + } + + /** + * Checks if the current user is this user + * + * @return bool + */ + public function isLoggedIn(): bool + { + return $this->is($this->kirby()->user()); + } + + /** + * Checks if the user is the last one + * with the admin role + * + * @return bool + */ + public function isLastAdmin(): bool + { + return $this->role()->isAdmin() === true && + $this->kirby()->users()->filter('role', 'admin')->count() <= 1; + } + + /** + * Checks if the user is the last user + * + * @return bool + */ + public function isLastUser(): bool + { + return $this->kirby()->users()->count() === 1; + } + + /** + * Checks if the current user is the virtual + * Nobody user + * + * @return bool + */ + public function isNobody(): bool + { + return $this->email() === 'nobody@getkirby.com'; + } + + /** + * Returns the user language + * + * @return string + */ + public function language(): string + { + return $this->language ??= $this->credentials()['language'] ?? $this->kirby()->panelLanguage(); + } + + /** + * Logs the user in + * + * @param string $password + * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in + * @return bool + */ + public function login(string $password, $session = null): bool + { + $this->validatePassword($password); + $this->loginPasswordless($session); + + return true; + } + + /** + * Logs the user in without checking the password + * + * @param \Kirby\Session\Session|array|null $session Session options or session object to set the user in + * @return void + */ + public function loginPasswordless($session = null): void + { + $kirby = $this->kirby(); + + $session = $this->sessionFromOptions($session); + + $kirby->trigger('user.login:before', ['user' => $this, 'session' => $session]); + + $session->regenerateToken(); // privilege change + $session->data()->set('kirby.userId', $this->id()); + $this->kirby()->auth()->setUser($this); + + $kirby->trigger('user.login:after', ['user' => $this, 'session' => $session]); + } + + /** + * Logs the user out + * + * @param \Kirby\Session\Session|array|null $session Session options or session object to unset the user in + * @return void + */ + public function logout($session = null): void + { + $kirby = $this->kirby(); + $session = $this->sessionFromOptions($session); + + $kirby->trigger('user.logout:before', ['user' => $this, 'session' => $session]); + + // remove the user from the session for future requests + $session->data()->remove('kirby.userId'); + + // clear the cached user object from the app state of the current request + $this->kirby()->auth()->flush(); + + if ($session->data()->get() === []) { + // session is now empty, we might as well destroy it + $session->destroy(); + + $kirby->trigger('user.logout:after', ['user' => $this, 'session' => null]); + } else { + // privilege change + $session->regenerateToken(); + + $kirby->trigger('user.logout:after', ['user' => $this, 'session' => $session]); + } + } + + /** + * Returns the root to the media folder for the user + * + * @internal + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/users/' . $this->id(); + } + + /** + * Returns the media url for the user object + * + * @internal + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/users/' . $this->id(); + } + + /** + * Creates a user model if it has been registered + * + * @internal + * @param string $name + * @param array $props + * @return \Kirby\Cms\User + */ + public static function model(string $name, array $props = []) + { + if ($class = (static::$models[$name] ?? null)) { + $object = new $class($props); + + if (is_a($object, 'Kirby\Cms\User') === true) { + return $object; + } + } + + return new static($props); + } + + /** + * Returns the last modification date of the user + * + * @param string $format + * @param string|null $handler + * @param string|null $languageCode + * @return int|string + */ + public function modified(string $format = 'U', string $handler = null, string $languageCode = null) + { + $modifiedContent = F::modified($this->contentFile($languageCode)); + $modifiedIndex = F::modified($this->root() . '/index.php'); + $modifiedTotal = max([$modifiedContent, $modifiedIndex]); + $handler ??= $this->kirby()->option('date.handler', 'date'); + + return Str::date($modifiedTotal, $format, $handler); + } + + /** + * Returns the user's name + * + * @return \Kirby\Cms\Field + */ + public function name() + { + if (is_string($this->name) === true) { + return new Field($this, 'name', $this->name); + } + + if ($this->name !== null) { + return $this->name; + } + + return $this->name = new Field($this, 'name', $this->credentials()['name'] ?? null); + } + + /** + * Returns the user's name or, + * if empty, the email address + * + * @return \Kirby\Cms\Field + */ + public function nameOrEmail() + { + $name = $this->name(); + return $name->isNotEmpty() ? $name : new Field($this, 'email', $this->email()); + } + + /** + * Create a dummy nobody + * + * @internal + * @return static + */ + public static function nobody() + { + return new static([ + 'email' => 'nobody@getkirby.com', + 'role' => 'nobody' + ]); + } + + /** + * Returns the panel info object + * + * @return \Kirby\Panel\User + */ + public function panel() + { + return new Panel($this); + } + + /** + * Returns the encrypted user password + * + * @return string|null + */ + public function password(): ?string + { + if ($this->password !== null) { + return $this->password; + } + + return $this->password = $this->readPassword(); + } + + /** + * @return \Kirby\Cms\UserPermissions + */ + public function permissions() + { + return new UserPermissions($this); + } + + /** + * Returns the user role + * + * @return \Kirby\Cms\Role + */ + public function role() + { + if (is_a($this->role, 'Kirby\Cms\Role') === true) { + return $this->role; + } + + $roleName = $this->role ?? $this->credentials()['role'] ?? 'visitor'; + + if ($role = $this->kirby()->roles()->find($roleName)) { + return $this->role = $role; + } + + return $this->role = Role::nobody(); + } + + /** + * Returns all available roles + * for this user, that can be selected + * by the authenticated user + * + * @return \Kirby\Cms\Roles + */ + public function roles() + { + $kirby = $this->kirby(); + $roles = $kirby->roles(); + + // a collection with just the one role of the user + $myRole = $roles->filter('id', $this->role()->id()); + + // if there's an authenticated user … + if ($user = $kirby->user()) { + // admin users can select pretty much any role + if ($user->isAdmin() === true) { + // except if the user is the last admin + if ($this->isLastAdmin() === true) { + // in which case they have to stay admin + return $myRole; + } + + // return all roles for mighty admins + return $roles; + } + } + + // any other user can only keep their role + return $myRole; + } + + /** + * The absolute path to the user directory + * + * @return string + */ + public function root(): string + { + return $this->kirby()->root('accounts') . '/' . $this->id(); + } + + /** + * Returns the UserRules class to + * validate any important action. + * + * @return \Kirby\Cms\UserRules + */ + protected function rules() + { + return new UserRules(); + } + + /** + * Sets the Blueprint object + * + * @param array|null $blueprint + * @return $this + */ + protected function setBlueprint(array $blueprint = null) + { + if ($blueprint !== null) { + $blueprint['model'] = $this; + $this->blueprint = new UserBlueprint($blueprint); + } + + return $this; + } + + /** + * Sets the user email + * + * @param string $email|null + * @return $this + */ + protected function setEmail(string $email = null) + { + if ($email !== null) { + $this->email = Str::lower(trim($email)); + } + return $this; + } + + /** + * Sets the user id + * + * @param string $id|null + * @return $this + */ + protected function setId(string $id = null) + { + $this->id = $id; + return $this; + } + + /** + * Sets the user language + * + * @param string $language|null + * @return $this + */ + protected function setLanguage(string $language = null) + { + $this->language = $language !== null ? trim($language) : null; + return $this; + } + + /** + * Sets the user name + * + * @param string $name|null + * @return $this + */ + protected function setName(string $name = null) + { + $this->name = $name !== null ? trim(strip_tags($name)) : null; + return $this; + } + + /** + * Sets the user's password hash + * + * @param string $password|null + * @return $this + */ + protected function setPassword(string $password = null) + { + $this->password = $password; + return $this; + } + + /** + * Sets the user role + * + * @param string $role|null + * @return $this + */ + protected function setRole(string $role = null) + { + $this->role = $role !== null ? Str::lower(trim($role)) : null; + return $this; + } + + /** + * Converts session options into a session object + * + * @param \Kirby\Session\Session|array $session Session options or session object to unset the user in + * @return \Kirby\Session\Session + */ + protected function sessionFromOptions($session) + { + // use passed session options or session object if set + if (is_array($session) === true) { + $session = $this->kirby()->session($session); + } elseif (is_a($session, 'Kirby\Session\Session') === false) { + $session = $this->kirby()->session(['detect' => true]); + } + + return $session; + } + + /** + * Returns the parent Users collection + * + * @return \Kirby\Cms\Users + */ + protected function siblingsCollection() + { + return $this->kirby()->users(); + } + + /** + * Converts the most important user properties + * to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'avatar' => $this->avatar() ? $this->avatar()->toArray() : null, + 'content' => $this->content()->toArray(), + 'email' => $this->email(), + 'id' => $this->id(), + 'language' => $this->language(), + 'role' => $this->role()->name(), + 'username' => $this->username() + ]; + } + + /** + * String template builder + * + * @param string|null $template + * @param array|null $data + * @param string $fallback Fallback for tokens in the template that cannot be replaced + * @return string + */ + public function toString(string $template = null, array $data = [], string $fallback = '', string $handler = 'template'): string + { + if ($template === null) { + $template = $this->email(); + } + + return parent::toString($template, $data, $fallback, $handler); + } + + /** + * Returns the username + * which is the given name or the email + * as a fallback + * + * @return string|null + */ + public function username(): ?string + { + return $this->name()->or($this->email())->value(); + } + + /** + * Compares the given password with the stored one + * + * @param string $password|null + * @return bool + * + * @throws \Kirby\Exception\NotFoundException If the user has no password + * @throws \Kirby\Exception\InvalidArgumentException If the entered password is not valid + * or does not match the user password + */ + public function validatePassword(string $password = null): bool + { + if (empty($this->password()) === true) { + throw new NotFoundException(['key' => 'user.password.undefined']); + } + + if (Str::length($password) < 8) { + throw new InvalidArgumentException(['key' => 'user.password.invalid']); + } + + if (password_verify($password, $this->password()) !== true) { + throw new InvalidArgumentException(['key' => 'user.password.wrong', 'httpCode' => 401]); + } + + return true; + } + + + /** + * Deprecated! + */ + + /** + * Returns the full path without leading slash + * + * @todo Remove in 3.8.0 + * + * @internal + * @return string + * @codeCoverageIgnore + */ + public function panelPath(): string + { + Helpers::deprecated('Cms\User::panelPath() has been deprecated and will be removed in Kirby 3.8.0. Use $user->panel()->path() instead.'); + return $this->panel()->path(); + } + + /** + * Returns prepared data for the panel user picker + * + * @todo Remove in 3.8.0 + * + * @param array|null $params + * @return array + * @codeCoverageIgnore + */ + public function panelPickerData(array $params = null): array + { + Helpers::deprecated('Cms\User::panelPickerData() has been deprecated and will be removed in Kirby 3.8.0. Use $user->panel()->pickerData() instead.'); + return $this->panel()->pickerData($params); + } + + /** + * Returns the url to the editing view + * in the panel + * + * @todo Remove in 3.8.0 + * + * @internal + * @param bool $relative + * @return string + * @codeCoverageIgnore + */ + public function panelUrl(bool $relative = false): string + { + Helpers::deprecated('Cms\User::panelUrl() has been deprecated and will be removed in Kirby 3.8.0. Use $user->panel()->url() instead.'); + return $this->panel()->url($relative); + } } diff --git a/kirby/src/Cms/UserActions.php b/kirby/src/Cms/UserActions.php index 8a5df12..4547844 100644 --- a/kirby/src/Cms/UserActions.php +++ b/kirby/src/Cms/UserActions.php @@ -24,367 +24,367 @@ use Throwable; */ trait UserActions { - /** - * Changes the user email address - * - * @param string $email - * @return static - */ - public function changeEmail(string $email) - { - $email = trim($email); + /** + * Changes the user email address + * + * @param string $email + * @return static + */ + public function changeEmail(string $email) + { + $email = trim($email); - return $this->commit('changeEmail', ['user' => $this, 'email' => Idn::decodeEmail($email)], function ($user, $email) { - $user = $user->clone([ - 'email' => $email - ]); + return $this->commit('changeEmail', ['user' => $this, 'email' => Idn::decodeEmail($email)], function ($user, $email) { + $user = $user->clone([ + 'email' => $email + ]); - $user->updateCredentials([ - 'email' => $email - ]); + $user->updateCredentials([ + 'email' => $email + ]); - // update the users collection - $user->kirby()->users()->set($user->id(), $user); + // update the users collection + $user->kirby()->users()->set($user->id(), $user); - return $user; - }); - } + return $user; + }); + } - /** - * Changes the user language - * - * @param string $language - * @return static - */ - public function changeLanguage(string $language) - { - return $this->commit('changeLanguage', ['user' => $this, 'language' => $language], function ($user, $language) { - $user = $user->clone([ - 'language' => $language, - ]); + /** + * Changes the user language + * + * @param string $language + * @return static + */ + public function changeLanguage(string $language) + { + return $this->commit('changeLanguage', ['user' => $this, 'language' => $language], function ($user, $language) { + $user = $user->clone([ + 'language' => $language, + ]); - $user->updateCredentials([ - 'language' => $language - ]); + $user->updateCredentials([ + 'language' => $language + ]); - // update the users collection - $user->kirby()->users()->set($user->id(), $user); + // update the users collection + $user->kirby()->users()->set($user->id(), $user); - return $user; - }); - } + return $user; + }); + } - /** - * Changes the screen name of the user - * - * @param string $name - * @return static - */ - public function changeName(string $name) - { - $name = trim($name); + /** + * Changes the screen name of the user + * + * @param string $name + * @return static + */ + public function changeName(string $name) + { + $name = trim($name); - return $this->commit('changeName', ['user' => $this, 'name' => $name], function ($user, $name) { - $user = $user->clone([ - 'name' => $name - ]); + return $this->commit('changeName', ['user' => $this, 'name' => $name], function ($user, $name) { + $user = $user->clone([ + 'name' => $name + ]); - $user->updateCredentials([ - 'name' => $name - ]); + $user->updateCredentials([ + 'name' => $name + ]); - // update the users collection - $user->kirby()->users()->set($user->id(), $user); + // update the users collection + $user->kirby()->users()->set($user->id(), $user); - return $user; - }); - } + return $user; + }); + } - /** - * Changes the user password - * - * @param string $password - * @return static - */ - public function changePassword(string $password) - { - return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) { - $user = $user->clone([ - 'password' => $password = User::hashPassword($password) - ]); + /** + * Changes the user password + * + * @param string $password + * @return static + */ + public function changePassword(string $password) + { + return $this->commit('changePassword', ['user' => $this, 'password' => $password], function ($user, $password) { + $user = $user->clone([ + 'password' => $password = User::hashPassword($password) + ]); - $user->writePassword($password); + $user->writePassword($password); - // update the users collection - $user->kirby()->users()->set($user->id(), $user); + // update the users collection + $user->kirby()->users()->set($user->id(), $user); - return $user; - }); - } + return $user; + }); + } - /** - * Changes the user role - * - * @param string $role - * @return static - */ - public function changeRole(string $role) - { - return $this->commit('changeRole', ['user' => $this, 'role' => $role], function ($user, $role) { - $user = $user->clone([ - 'role' => $role, - ]); + /** + * Changes the user role + * + * @param string $role + * @return static + */ + public function changeRole(string $role) + { + return $this->commit('changeRole', ['user' => $this, 'role' => $role], function ($user, $role) { + $user = $user->clone([ + 'role' => $role, + ]); - $user->updateCredentials([ - 'role' => $role - ]); + $user->updateCredentials([ + 'role' => $role + ]); - // update the users collection - $user->kirby()->users()->set($user->id(), $user); + // update the users collection + $user->kirby()->users()->set($user->id(), $user); - return $user; - }); - } + return $user; + }); + } - /** - * Commits a user action, by following these steps - * - * 1. checks the action rules - * 2. sends the before hook - * 3. commits the action - * 4. sends the after hook - * 5. returns the result - * - * @param string $action - * @param array $arguments - * @param \Closure $callback - * @return mixed - * @throws \Kirby\Exception\PermissionException - */ - protected function commit(string $action, array $arguments, Closure $callback) - { - if ($this->isKirby() === true) { - throw new PermissionException('The Kirby user cannot be changed'); - } + /** + * Commits a user action, by following these steps + * + * 1. checks the action rules + * 2. sends the before hook + * 3. commits the action + * 4. sends the after hook + * 5. returns the result + * + * @param string $action + * @param array $arguments + * @param \Closure $callback + * @return mixed + * @throws \Kirby\Exception\PermissionException + */ + protected function commit(string $action, array $arguments, Closure $callback) + { + if ($this->isKirby() === true) { + throw new PermissionException('The Kirby user cannot be changed'); + } - $old = $this->hardcopy(); - $kirby = $this->kirby(); - $argumentValues = array_values($arguments); + $old = $this->hardcopy(); + $kirby = $this->kirby(); + $argumentValues = array_values($arguments); - $this->rules()->$action(...$argumentValues); - $kirby->trigger('user.' . $action . ':before', $arguments); + $this->rules()->$action(...$argumentValues); + $kirby->trigger('user.' . $action . ':before', $arguments); - $result = $callback(...$argumentValues); + $result = $callback(...$argumentValues); - if ($action === 'create') { - $argumentsAfter = ['user' => $result]; - } elseif ($action === 'delete') { - $argumentsAfter = ['status' => $result, 'user' => $old]; - } else { - $argumentsAfter = ['newUser' => $result, 'oldUser' => $old]; - } - $kirby->trigger('user.' . $action . ':after', $argumentsAfter); + if ($action === 'create') { + $argumentsAfter = ['user' => $result]; + } elseif ($action === 'delete') { + $argumentsAfter = ['status' => $result, 'user' => $old]; + } else { + $argumentsAfter = ['newUser' => $result, 'oldUser' => $old]; + } + $kirby->trigger('user.' . $action . ':after', $argumentsAfter); - $kirby->cache('pages')->flush(); - return $result; - } + $kirby->cache('pages')->flush(); + return $result; + } - /** - * Creates a new User from the given props and returns a new User object - * - * @param array|null $props - * @return static - */ - public static function create(array $props = null) - { - $data = $props; + /** + * Creates a new User from the given props and returns a new User object + * + * @param array|null $props + * @return static + */ + public static function create(array $props = null) + { + $data = $props; - if (isset($props['email']) === true) { - $data['email'] = Idn::decodeEmail($props['email']); - } + if (isset($props['email']) === true) { + $data['email'] = Idn::decodeEmail($props['email']); + } - if (isset($props['password']) === true) { - $data['password'] = User::hashPassword($props['password']); - } + if (isset($props['password']) === true) { + $data['password'] = User::hashPassword($props['password']); + } - $props['role'] = $props['model'] = strtolower($props['role'] ?? 'default'); + $props['role'] = $props['model'] = strtolower($props['role'] ?? 'default'); - $user = User::factory($data); + $user = User::factory($data); - // create a form for the user - $form = Form::for($user, [ - 'values' => $props['content'] ?? [] - ]); + // create a form for the user + $form = Form::for($user, [ + 'values' => $props['content'] ?? [] + ]); - // inject the content - $user = $user->clone(['content' => $form->strings(true)]); + // inject the content + $user = $user->clone(['content' => $form->strings(true)]); - // run the hook - return $user->commit('create', ['user' => $user, 'input' => $props], function ($user, $props) { - $user->writeCredentials([ - 'email' => $user->email(), - 'language' => $user->language(), - 'name' => $user->name()->value(), - 'role' => $user->role()->id(), - ]); + // run the hook + return $user->commit('create', ['user' => $user, 'input' => $props], function ($user, $props) { + $user->writeCredentials([ + 'email' => $user->email(), + 'language' => $user->language(), + 'name' => $user->name()->value(), + 'role' => $user->role()->id(), + ]); - $user->writePassword($user->password()); + $user->writePassword($user->password()); - // always create users in the default language - if ($user->kirby()->multilang() === true) { - $languageCode = $user->kirby()->defaultLanguage()->code(); - } else { - $languageCode = null; - } + // always create users in the default language + if ($user->kirby()->multilang() === true) { + $languageCode = $user->kirby()->defaultLanguage()->code(); + } else { + $languageCode = null; + } - // add the user to users collection - $user->kirby()->users()->add($user); + // add the user to users collection + $user->kirby()->users()->add($user); - // write the user data - return $user->save($user->content()->toArray(), $languageCode); - }); - } + // write the user data + return $user->save($user->content()->toArray(), $languageCode); + }); + } - /** - * Returns a random user id - * - * @return string - */ - public function createId(): string - { - $length = 8; + /** + * Returns a random user id + * + * @return string + */ + public function createId(): string + { + $length = 8; - do { - try { - $id = Str::random($length); - if (UserRules::validId($this, $id) === true) { - return $id; - } + do { + try { + $id = Str::random($length); + if (UserRules::validId($this, $id) === true) { + return $id; + } - // we can't really test for a random match - // @codeCoverageIgnoreStart - } catch (Throwable $e) { - $length++; - } - } while (true); - // @codeCoverageIgnoreEnd - } + // we can't really test for a random match + // @codeCoverageIgnoreStart + } catch (Throwable $e) { + $length++; + } + } while (true); + // @codeCoverageIgnoreEnd + } - /** - * Deletes the user - * - * @return bool - * @throws \Kirby\Exception\LogicException - */ - public function delete(): bool - { - return $this->commit('delete', ['user' => $this], function ($user) { - if ($user->exists() === false) { - return true; - } + /** + * Deletes the user + * + * @return bool + * @throws \Kirby\Exception\LogicException + */ + public function delete(): bool + { + return $this->commit('delete', ['user' => $this], function ($user) { + if ($user->exists() === false) { + return true; + } - // delete all public assets for this user - Dir::remove($user->mediaRoot()); + // delete all public assets for this user + Dir::remove($user->mediaRoot()); - // delete the user directory - if (Dir::remove($user->root()) !== true) { - throw new LogicException('The user directory for "' . $user->email() . '" could not be deleted'); - } + // delete the user directory + if (Dir::remove($user->root()) !== true) { + throw new LogicException('The user directory for "' . $user->email() . '" could not be deleted'); + } - // remove the user from users collection - $user->kirby()->users()->remove($user); + // remove the user from users collection + $user->kirby()->users()->remove($user); - return true; - }); - } + return true; + }); + } - /** - * Read the account information from disk - * - * @return array - */ - protected function readCredentials(): array - { - $path = $this->root() . '/index.php'; + /** + * Read the account information from disk + * + * @return array + */ + protected function readCredentials(): array + { + $path = $this->root() . '/index.php'; - if (is_file($path) === true) { - $credentials = F::load($path); + if (is_file($path) === true) { + $credentials = F::load($path); - return is_array($credentials) === false ? [] : $credentials; - } else { - return []; - } - } + return is_array($credentials) === false ? [] : $credentials; + } else { + return []; + } + } - /** - * Reads the user password from disk - * - * @return string|false - */ - protected function readPassword() - { - return F::read($this->root() . '/.htpasswd'); - } + /** + * Reads the user password from disk + * + * @return string|false + */ + protected function readPassword() + { + return F::read($this->root() . '/.htpasswd'); + } - /** - * Updates the user data - * - * @param array|null $input - * @param string|null $languageCode - * @param bool $validate - * @return static - */ - public function update(array $input = null, string $languageCode = null, bool $validate = false) - { - $user = parent::update($input, $languageCode, $validate); + /** + * Updates the user data + * + * @param array|null $input + * @param string|null $languageCode + * @param bool $validate + * @return static + */ + public function update(array $input = null, string $languageCode = null, bool $validate = false) + { + $user = parent::update($input, $languageCode, $validate); - // set auth user data only if the current user is this user - if ($user->isLoggedIn() === true) { - $this->kirby()->auth()->setUser($user); - } + // set auth user data only if the current user is this user + if ($user->isLoggedIn() === true) { + $this->kirby()->auth()->setUser($user); + } - // update the users collection - $user->kirby()->users()->set($user->id(), $user); + // update the users collection + $user->kirby()->users()->set($user->id(), $user); - return $user; - } + return $user; + } - /** - * This always merges the existing credentials - * with the given input. - * - * @param array $credentials - * @return bool - */ - protected function updateCredentials(array $credentials): bool - { - // normalize the email address - if (isset($credentials['email']) === true) { - $credentials['email'] = Str::lower(trim($credentials['email'])); - } + /** + * This always merges the existing credentials + * with the given input. + * + * @param array $credentials + * @return bool + */ + protected function updateCredentials(array $credentials): bool + { + // normalize the email address + if (isset($credentials['email']) === true) { + $credentials['email'] = Str::lower(trim($credentials['email'])); + } - return $this->writeCredentials(array_merge($this->credentials(), $credentials)); - } + return $this->writeCredentials(array_merge($this->credentials(), $credentials)); + } - /** - * Writes the account information to disk - * - * @param array $credentials - * @return bool - */ - protected function writeCredentials(array $credentials): bool - { - return Data::write($this->root() . '/index.php', $credentials); - } + /** + * Writes the account information to disk + * + * @param array $credentials + * @return bool + */ + protected function writeCredentials(array $credentials): bool + { + return Data::write($this->root() . '/index.php', $credentials); + } - /** - * Writes the password to disk - * - * @param string|null $password - * @return bool - */ - protected function writePassword(string $password = null): bool - { - return F::write($this->root() . '/.htpasswd', $password); - } + /** + * Writes the password to disk + * + * @param string|null $password + * @return bool + */ + protected function writePassword(string $password = null): bool + { + return F::write($this->root() . '/.htpasswd', $password); + } } diff --git a/kirby/src/Cms/UserBlueprint.php b/kirby/src/Cms/UserBlueprint.php index 1552ef0..0f86fe1 100644 --- a/kirby/src/Cms/UserBlueprint.php +++ b/kirby/src/Cms/UserBlueprint.php @@ -14,34 +14,34 @@ namespace Kirby\Cms; */ class UserBlueprint extends Blueprint { - /** - * UserBlueprint constructor. - * - * @param array $props - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function __construct(array $props) - { - // normalize and translate the description - $props['description'] = $this->i18n($props['description'] ?? null); + /** + * UserBlueprint constructor. + * + * @param array $props + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(array $props) + { + // normalize and translate the description + $props['description'] = $this->i18n($props['description'] ?? null); - // register the other props - parent::__construct($props); + // register the other props + parent::__construct($props); - // normalize all available page options - $this->props['options'] = $this->normalizeOptions( - $props['options'] ?? true, - // defaults - [ - 'create' => null, - 'changeEmail' => null, - 'changeLanguage' => null, - 'changeName' => null, - 'changePassword' => null, - 'changeRole' => null, - 'delete' => null, - 'update' => null, - ] - ); - } + // normalize all available page options + $this->props['options'] = $this->normalizeOptions( + $this->props['options'] ?? true, + // defaults + [ + 'create' => null, + 'changeEmail' => null, + 'changeLanguage' => null, + 'changeName' => null, + 'changePassword' => null, + 'changeRole' => null, + 'delete' => null, + 'update' => null, + ] + ); + } } diff --git a/kirby/src/Cms/UserPermissions.php b/kirby/src/Cms/UserPermissions.php index 34e0176..bc91161 100644 --- a/kirby/src/Cms/UserPermissions.php +++ b/kirby/src/Cms/UserPermissions.php @@ -13,55 +13,55 @@ namespace Kirby\Cms; */ class UserPermissions extends ModelPermissions { - /** - * @var string - */ - protected $category = 'users'; + /** + * @var string + */ + protected $category = 'users'; - /** - * UserPermissions constructor - * - * @param \Kirby\Cms\Model $model - */ - public function __construct(Model $model) - { - parent::__construct($model); + /** + * UserPermissions constructor + * + * @param \Kirby\Cms\Model $model + */ + public function __construct(Model $model) + { + parent::__construct($model); - // change the scope of the permissions, when the current user is this user - $this->category = $this->user && $this->user->is($model) ? 'user' : 'users'; - } + // change the scope of the permissions, when the current user is this user + $this->category = $this->user && $this->user->is($model) ? 'user' : 'users'; + } - /** - * @return bool - */ - protected function canChangeRole(): bool - { - return $this->model->roles()->count() > 1; - } + /** + * @return bool + */ + protected function canChangeRole(): bool + { + return $this->model->roles()->count() > 1; + } - /** - * @return bool - */ - protected function canCreate(): bool - { - // the admin can always create new users - if ($this->user->isAdmin() === true) { - return true; - } + /** + * @return bool + */ + protected function canCreate(): bool + { + // the admin can always create new users + if ($this->user->isAdmin() === true) { + return true; + } - // users who are not admins cannot create admins - if ($this->model->isAdmin() === true) { - return false; - } + // users who are not admins cannot create admins + if ($this->model->isAdmin() === true) { + return false; + } - return true; - } + return true; + } - /** - * @return bool - */ - protected function canDelete(): bool - { - return $this->model->isLastAdmin() !== true; - } + /** + * @return bool + */ + protected function canDelete(): bool + { + return $this->model->isLastAdmin() !== true; + } } diff --git a/kirby/src/Cms/UserPicker.php b/kirby/src/Cms/UserPicker.php index f4c01ec..46da3dc 100644 --- a/kirby/src/Cms/UserPicker.php +++ b/kirby/src/Cms/UserPicker.php @@ -17,53 +17,53 @@ use Kirby\Exception\InvalidArgumentException; */ class UserPicker extends Picker { - /** - * Extends the basic defaults - * - * @return array - */ - public function defaults(): array - { - $defaults = parent::defaults(); - $defaults['text'] = '{{ user.username }}'; + /** + * Extends the basic defaults + * + * @return array + */ + public function defaults(): array + { + $defaults = parent::defaults(); + $defaults['text'] = '{{ user.username }}'; - return $defaults; - } + return $defaults; + } - /** - * Search all users for the picker - * - * @return \Kirby\Cms\Users|null - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function items() - { - $model = $this->options['model']; + /** + * Search all users for the picker + * + * @return \Kirby\Cms\Users|null + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function items() + { + $model = $this->options['model']; - // find the right default query - if (empty($this->options['query']) === false) { - $query = $this->options['query']; - } elseif (is_a($model, 'Kirby\Cms\User') === true) { - $query = 'user.siblings'; - } else { - $query = 'kirby.users'; - } + // find the right default query + if (empty($this->options['query']) === false) { + $query = $this->options['query']; + } elseif (is_a($model, 'Kirby\Cms\User') === true) { + $query = 'user.siblings'; + } else { + $query = 'kirby.users'; + } - // fetch all users for the picker - $users = $model->query($query); + // fetch all users for the picker + $users = $model->query($query); - // catch invalid data - if (is_a($users, 'Kirby\Cms\Users') === false) { - throw new InvalidArgumentException('Your query must return a set of users'); - } + // catch invalid data + if (is_a($users, 'Kirby\Cms\Users') === false) { + throw new InvalidArgumentException('Your query must return a set of users'); + } - // search - $users = $this->search($users); + // search + $users = $this->search($users); - // sort - $users = $users->sort('username', 'asc'); + // sort + $users = $users->sort('username', 'asc'); - // paginate - return $this->paginate($users); - } + // paginate + return $this->paginate($users); + } } diff --git a/kirby/src/Cms/UserRules.php b/kirby/src/Cms/UserRules.php index 34d554b..fa740fa 100644 --- a/kirby/src/Cms/UserRules.php +++ b/kirby/src/Cms/UserRules.php @@ -20,350 +20,350 @@ use Kirby\Toolkit\V; */ class UserRules { - /** - * Validates if the email address can be changed - * - * @param \Kirby\Cms\User $user - * @param string $email - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the address - */ - public static function changeEmail(User $user, string $email): bool - { - if ($user->permissions()->changeEmail() !== true) { - throw new PermissionException([ - 'key' => 'user.changeEmail.permission', - 'data' => ['name' => $user->username()] - ]); - } + /** + * Validates if the email address can be changed + * + * @param \Kirby\Cms\User $user + * @param string $email + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the address + */ + public static function changeEmail(User $user, string $email): bool + { + if ($user->permissions()->changeEmail() !== true) { + throw new PermissionException([ + 'key' => 'user.changeEmail.permission', + 'data' => ['name' => $user->username()] + ]); + } - return static::validEmail($user, $email); - } + return static::validEmail($user, $email); + } - /** - * Validates if the language can be changed - * - * @param \Kirby\Cms\User $user - * @param string $language - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the language - */ - public static function changeLanguage(User $user, string $language): bool - { - if ($user->permissions()->changeLanguage() !== true) { - throw new PermissionException([ - 'key' => 'user.changeLanguage.permission', - 'data' => ['name' => $user->username()] - ]); - } + /** + * Validates if the language can be changed + * + * @param \Kirby\Cms\User $user + * @param string $language + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the language + */ + public static function changeLanguage(User $user, string $language): bool + { + if ($user->permissions()->changeLanguage() !== true) { + throw new PermissionException([ + 'key' => 'user.changeLanguage.permission', + 'data' => ['name' => $user->username()] + ]); + } - return static::validLanguage($user, $language); - } + return static::validLanguage($user, $language); + } - /** - * Validates if the name can be changed - * - * @param \Kirby\Cms\User $user - * @param string $name - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the name - */ - public static function changeName(User $user, string $name): bool - { - if ($user->permissions()->changeName() !== true) { - throw new PermissionException([ - 'key' => 'user.changeName.permission', - 'data' => ['name' => $user->username()] - ]); - } + /** + * Validates if the name can be changed + * + * @param \Kirby\Cms\User $user + * @param string $name + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the name + */ + public static function changeName(User $user, string $name): bool + { + if ($user->permissions()->changeName() !== true) { + throw new PermissionException([ + 'key' => 'user.changeName.permission', + 'data' => ['name' => $user->username()] + ]); + } - return true; - } + return true; + } - /** - * Validates if the password can be changed - * - * @param \Kirby\Cms\User $user - * @param string $password - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the password - */ - public static function changePassword(User $user, string $password): bool - { - if ($user->permissions()->changePassword() !== true) { - throw new PermissionException([ - 'key' => 'user.changePassword.permission', - 'data' => ['name' => $user->username()] - ]); - } + /** + * Validates if the password can be changed + * + * @param \Kirby\Cms\User $user + * @param string $password + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the password + */ + public static function changePassword(User $user, string $password): bool + { + if ($user->permissions()->changePassword() !== true) { + throw new PermissionException([ + 'key' => 'user.changePassword.permission', + 'data' => ['name' => $user->username()] + ]); + } - return static::validPassword($user, $password); - } + return static::validPassword($user, $password); + } - /** - * Validates if the role can be changed - * - * @param \Kirby\Cms\User $user - * @param string $role - * @return bool - * @throws \Kirby\Exception\LogicException If the user is the last admin - * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the role - */ - public static function changeRole(User $user, string $role): bool - { - // protect admin from role changes by non-admin - if ( - $user->kirby()->user()->isAdmin() === false && - $user->isAdmin() === true - ) { - throw new PermissionException([ - 'key' => 'user.changeRole.permission', - 'data' => ['name' => $user->username()] - ]); - } + /** + * Validates if the role can be changed + * + * @param \Kirby\Cms\User $user + * @param string $role + * @return bool + * @throws \Kirby\Exception\LogicException If the user is the last admin + * @throws \Kirby\Exception\PermissionException If the user is not allowed to change the role + */ + public static function changeRole(User $user, string $role): bool + { + // protect admin from role changes by non-admin + if ( + $user->kirby()->user()->isAdmin() === false && + $user->isAdmin() === true + ) { + throw new PermissionException([ + 'key' => 'user.changeRole.permission', + 'data' => ['name' => $user->username()] + ]); + } - // prevent non-admins making a user to admin - if ( - $user->kirby()->user()->isAdmin() === false && - $role === 'admin' - ) { - throw new PermissionException([ - 'key' => 'user.changeRole.toAdmin' - ]); - } + // prevent non-admins making a user to admin + if ( + $user->kirby()->user()->isAdmin() === false && + $role === 'admin' + ) { + throw new PermissionException([ + 'key' => 'user.changeRole.toAdmin' + ]); + } - static::validRole($user, $role); + static::validRole($user, $role); - if ($role !== 'admin' && $user->isLastAdmin() === true) { - throw new LogicException([ - 'key' => 'user.changeRole.lastAdmin', - 'data' => ['name' => $user->username()] - ]); - } + if ($role !== 'admin' && $user->isLastAdmin() === true) { + throw new LogicException([ + 'key' => 'user.changeRole.lastAdmin', + 'data' => ['name' => $user->username()] + ]); + } - if ($user->permissions()->changeRole() !== true) { - throw new PermissionException([ - 'key' => 'user.changeRole.permission', - 'data' => ['name' => $user->username()] - ]); - } + if ($user->permissions()->changeRole() !== true) { + throw new PermissionException([ + 'key' => 'user.changeRole.permission', + 'data' => ['name' => $user->username()] + ]); + } - return true; - } + return true; + } - /** - * Validates if the user can be created - * - * @param \Kirby\Cms\User $user - * @param array $props - * @return bool - * @throws \Kirby\Exception\PermissionException If the user is not allowed to create a new user - */ - public static function create(User $user, array $props = []): bool - { - static::validId($user, $user->id()); - static::validEmail($user, $user->email(), true); - static::validLanguage($user, $user->language()); + /** + * Validates if the user can be created + * + * @param \Kirby\Cms\User $user + * @param array $props + * @return bool + * @throws \Kirby\Exception\PermissionException If the user is not allowed to create a new user + */ + public static function create(User $user, array $props = []): bool + { + static::validId($user, $user->id()); + static::validEmail($user, $user->email(), true); + static::validLanguage($user, $user->language()); - // the first user must have a password - if ($user->kirby()->users()->count() === 0 && empty($props['password'])) { - // trigger invalid password error - static::validPassword($user, ' '); - } + // the first user must have a password + if ($user->kirby()->users()->count() === 0 && empty($props['password'])) { + // trigger invalid password error + static::validPassword($user, ' '); + } - if (empty($props['password']) === false) { - static::validPassword($user, $props['password']); - } + if (empty($props['password']) === false) { + static::validPassword($user, $props['password']); + } - // get the current user if it exists - $currentUser = $user->kirby()->user(); + // get the current user if it exists + $currentUser = $user->kirby()->user(); - // admins are allowed everything - if ($currentUser && $currentUser->isAdmin() === true) { - return true; - } + // admins are allowed everything + if ($currentUser && $currentUser->isAdmin() === true) { + return true; + } - // only admins are allowed to add admins - $role = $props['role'] ?? null; + // only admins are allowed to add admins + $role = $props['role'] ?? null; - if ($role === 'admin' && $currentUser && $currentUser->isAdmin() === false) { - throw new PermissionException([ - 'key' => 'user.create.permission' - ]); - } + if ($role === 'admin' && $currentUser && $currentUser->isAdmin() === false) { + throw new PermissionException([ + 'key' => 'user.create.permission' + ]); + } - // check user permissions (if not on install) - if ($user->kirby()->users()->count() > 0) { - if ($user->permissions()->create() !== true) { - throw new PermissionException([ - 'key' => 'user.create.permission' - ]); - } - } + // check user permissions (if not on install) + if ($user->kirby()->users()->count() > 0) { + if ($user->permissions()->create() !== true) { + throw new PermissionException([ + 'key' => 'user.create.permission' + ]); + } + } - return true; - } + return true; + } - /** - * Validates if the user can be deleted - * - * @param \Kirby\Cms\User $user - * @return bool - * @throws \Kirby\Exception\LogicException If this is the last user or last admin, which cannot be deleted - * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete this user - */ - public static function delete(User $user): bool - { - if ($user->isLastAdmin() === true) { - throw new LogicException(['key' => 'user.delete.lastAdmin']); - } + /** + * Validates if the user can be deleted + * + * @param \Kirby\Cms\User $user + * @return bool + * @throws \Kirby\Exception\LogicException If this is the last user or last admin, which cannot be deleted + * @throws \Kirby\Exception\PermissionException If the user is not allowed to delete this user + */ + public static function delete(User $user): bool + { + if ($user->isLastAdmin() === true) { + throw new LogicException(['key' => 'user.delete.lastAdmin']); + } - if ($user->isLastUser() === true) { - throw new LogicException([ - 'key' => 'user.delete.lastUser' - ]); - } + if ($user->isLastUser() === true) { + throw new LogicException([ + 'key' => 'user.delete.lastUser' + ]); + } - if ($user->permissions()->delete() !== true) { - throw new PermissionException([ - 'key' => 'user.delete.permission', - 'data' => ['name' => $user->username()] - ]); - } + if ($user->permissions()->delete() !== true) { + throw new PermissionException([ + 'key' => 'user.delete.permission', + 'data' => ['name' => $user->username()] + ]); + } - return true; - } + return true; + } - /** - * Validates if the user can be updated - * - * @param \Kirby\Cms\User $user - * @param array $values - * @param array $strings - * @return bool - * @throws \Kirby\Exception\PermissionException If the user it not allowed to update this user - */ - public static function update(User $user, array $values = [], array $strings = []): bool - { - if ($user->permissions()->update() !== true) { - throw new PermissionException([ - 'key' => 'user.update.permission', - 'data' => ['name' => $user->username()] - ]); - } + /** + * Validates if the user can be updated + * + * @param \Kirby\Cms\User $user + * @param array $values + * @param array $strings + * @return bool + * @throws \Kirby\Exception\PermissionException If the user it not allowed to update this user + */ + public static function update(User $user, array $values = [], array $strings = []): bool + { + if ($user->permissions()->update() !== true) { + throw new PermissionException([ + 'key' => 'user.update.permission', + 'data' => ['name' => $user->username()] + ]); + } - return true; - } + return true; + } - /** - * Validates an email address - * - * @param \Kirby\Cms\User $user - * @param string $email - * @param bool $strict - * @return bool - * @throws \Kirby\Exception\DuplicateException If the email address already exists - * @throws \Kirby\Exception\InvalidArgumentException If the email address is invalid - */ - public static function validEmail(User $user, string $email, bool $strict = false): bool - { - if (V::email($email ?? null) === false) { - throw new InvalidArgumentException([ - 'key' => 'user.email.invalid', - ]); - } + /** + * Validates an email address + * + * @param \Kirby\Cms\User $user + * @param string $email + * @param bool $strict + * @return bool + * @throws \Kirby\Exception\DuplicateException If the email address already exists + * @throws \Kirby\Exception\InvalidArgumentException If the email address is invalid + */ + public static function validEmail(User $user, string $email, bool $strict = false): bool + { + if (V::email($email ?? null) === false) { + throw new InvalidArgumentException([ + 'key' => 'user.email.invalid', + ]); + } - if ($strict === true) { - $duplicate = $user->kirby()->users()->find($email); - } else { - $duplicate = $user->kirby()->users()->not($user)->find($email); - } + if ($strict === true) { + $duplicate = $user->kirby()->users()->find($email); + } else { + $duplicate = $user->kirby()->users()->not($user)->find($email); + } - if ($duplicate) { - throw new DuplicateException([ - 'key' => 'user.duplicate', - 'data' => ['email' => $email] - ]); - } + if ($duplicate) { + throw new DuplicateException([ + 'key' => 'user.duplicate', + 'data' => ['email' => $email] + ]); + } - return true; - } + return true; + } - /** - * Validates a user id - * - * @param \Kirby\Cms\User $user - * @param string $id - * @return bool - * @throws \Kirby\Exception\DuplicateException If the user already exists - */ - public static function validId(User $user, string $id): bool - { - if ($id === 'account') { - throw new InvalidArgumentException('"account" is a reserved word and cannot be used as user id'); - } + /** + * Validates a user id + * + * @param \Kirby\Cms\User $user + * @param string $id + * @return bool + * @throws \Kirby\Exception\DuplicateException If the user already exists + */ + public static function validId(User $user, string $id): bool + { + if ($id === 'account') { + throw new InvalidArgumentException('"account" is a reserved word and cannot be used as user id'); + } - if ($user->kirby()->users()->find($id)) { - throw new DuplicateException('A user with this id exists'); - } + if ($user->kirby()->users()->find($id)) { + throw new DuplicateException('A user with this id exists'); + } - return true; - } + return true; + } - /** - * Validates a user language code - * - * @param \Kirby\Cms\User $user - * @param string $language - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the language does not exist - */ - public static function validLanguage(User $user, string $language): bool - { - if (in_array($language, $user->kirby()->translations()->keys(), true) === false) { - throw new InvalidArgumentException([ - 'key' => 'user.language.invalid', - ]); - } + /** + * Validates a user language code + * + * @param \Kirby\Cms\User $user + * @param string $language + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the language does not exist + */ + public static function validLanguage(User $user, string $language): bool + { + if (in_array($language, $user->kirby()->translations()->keys(), true) === false) { + throw new InvalidArgumentException([ + 'key' => 'user.language.invalid', + ]); + } - return true; - } + return true; + } - /** - * Validates a password - * - * @param \Kirby\Cms\User $user - * @param string $password - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the password is too short - */ - public static function validPassword(User $user, string $password): bool - { - if (Str::length($password ?? null) < 8) { - throw new InvalidArgumentException([ - 'key' => 'user.password.invalid', - ]); - } + /** + * Validates a password + * + * @param \Kirby\Cms\User $user + * @param string $password + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the password is too short + */ + public static function validPassword(User $user, string $password): bool + { + if (Str::length($password ?? null) < 8) { + throw new InvalidArgumentException([ + 'key' => 'user.password.invalid', + ]); + } - return true; - } + return true; + } - /** - * Validates a user role - * - * @param \Kirby\Cms\User $user - * @param string $role - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the user role does not exist - */ - public static function validRole(User $user, string $role): bool - { - if (is_a($user->kirby()->roles()->find($role), 'Kirby\Cms\Role') === true) { - return true; - } + /** + * Validates a user role + * + * @param \Kirby\Cms\User $user + * @param string $role + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the user role does not exist + */ + public static function validRole(User $user, string $role): bool + { + if (is_a($user->kirby()->roles()->find($role), 'Kirby\Cms\Role') === true) { + return true; + } - throw new InvalidArgumentException([ - 'key' => 'user.role.invalid', - ]); - } + throw new InvalidArgumentException([ + 'key' => 'user.role.invalid', + ]); + } } diff --git a/kirby/src/Cms/Users.php b/kirby/src/Cms/Users.php index 907f88e..055863c 100644 --- a/kirby/src/Cms/Users.php +++ b/kirby/src/Cms/Users.php @@ -21,127 +21,128 @@ use Kirby\Toolkit\Str; */ class Users extends Collection { - /** - * All registered users methods - * - * @var array - */ - public static $methods = []; + /** + * All registered users methods + * + * @var array + */ + public static $methods = []; - public function create(array $data) - { - return User::create($data); - } + public function create(array $data) + { + return User::create($data); + } - /** - * Adds a single user or - * an entire second collection to the - * current collection - * - * @param \Kirby\Cms\Users|\Kirby\Cms\User|string $object - * @return $this - * @throws \Kirby\Exception\InvalidArgumentException When no `User` or `Users` object or an ID of an existing user is passed - */ - public function add($object) - { - // add a users collection - if (is_a($object, self::class) === true) { - $this->data = array_merge($this->data, $object->data); + /** + * Adds a single user or + * an entire second collection to the + * current collection + * + * @param \Kirby\Cms\Users|\Kirby\Cms\User|string $object + * @return $this + * @throws \Kirby\Exception\InvalidArgumentException When no `User` or `Users` object or an ID of an existing user is passed + */ + public function add($object) + { + // add a users collection + if (is_a($object, self::class) === true) { + $this->data = array_merge($this->data, $object->data); - // add a user by id - } elseif (is_string($object) === true && $user = App::instance()->user($object)) { - $this->__set($user->id(), $user); + // add a user by id + } elseif (is_string($object) === true && $user = App::instance()->user($object)) { + $this->__set($user->id(), $user); - // add a user object - } elseif (is_a($object, 'Kirby\Cms\User') === true) { - $this->__set($object->id(), $object); + // add a user object + } elseif (is_a($object, 'Kirby\Cms\User') === true) { + $this->__set($object->id(), $object); - // give a useful error message on invalid input; - // silently ignore "empty" values for compatibility with existing setups - } elseif (in_array($object, [null, false, true], true) !== true) { - throw new InvalidArgumentException('You must pass a Users or User object or an ID of an existing user to the Users collection'); - } + // give a useful error message on invalid input; + // silently ignore "empty" values for compatibility with existing setups + } elseif (in_array($object, [null, false, true], true) !== true) { + throw new InvalidArgumentException('You must pass a Users or User object or an ID of an existing user to the Users collection'); + } - return $this; - } + return $this; + } - /** - * Takes an array of user props and creates a nice and clean user collection from it - * - * @param array $users - * @param array $inject - * @return static - */ - public static function factory(array $users, array $inject = []) - { - $collection = new static(); + /** + * Takes an array of user props and creates a nice and clean user collection from it + * + * @param array $users + * @param array $inject + * @return static + */ + public static function factory(array $users, array $inject = []) + { + $collection = new static(); - // read all user blueprints - foreach ($users as $props) { - $user = User::factory($props + $inject); - $collection->set($user->id(), $user); - } + // read all user blueprints + foreach ($users as $props) { + $user = User::factory($props + $inject); + $collection->set($user->id(), $user); + } - return $collection; - } + return $collection; + } - /** - * Finds a user in the collection by id or email address - * - * @param string $key - * @return \Kirby\Cms\User|null - */ - public function findByKey(string $key) - { - if (Str::contains($key, '@') === true) { - return parent::findBy('email', Str::lower($key)); - } + /** + * Finds a user in the collection by ID or email address + * @internal Use `$users->find()` instead + * + * @param string $key + * @return \Kirby\Cms\User|null + */ + public function findByKey(string $key) + { + if (Str::contains($key, '@') === true) { + return parent::findBy('email', Str::lower($key)); + } - return parent::findByKey($key); - } + return parent::findByKey($key); + } - /** - * Loads a user from disk by passing the absolute path (root) - * - * @param string $root - * @param array $inject - * @return static - */ - public static function load(string $root, array $inject = []) - { - $users = new static(); + /** + * Loads a user from disk by passing the absolute path (root) + * + * @param string $root + * @param array $inject + * @return static + */ + public static function load(string $root, array $inject = []) + { + $users = new static(); - foreach (Dir::read($root) as $userDirectory) { - if (is_dir($root . '/' . $userDirectory) === false) { - continue; - } + foreach (Dir::read($root) as $userDirectory) { + if (is_dir($root . '/' . $userDirectory) === false) { + continue; + } - // get role information - $path = $root . '/' . $userDirectory . '/index.php'; - if (is_file($path) === true) { - $credentials = F::load($path); - } + // get role information + $path = $root . '/' . $userDirectory . '/index.php'; + if (is_file($path) === true) { + $credentials = F::load($path); + } - // create user model based on role - $user = User::factory([ - 'id' => $userDirectory, - 'model' => $credentials['role'] ?? null - ] + $inject); + // create user model based on role + $user = User::factory([ + 'id' => $userDirectory, + 'model' => $credentials['role'] ?? null + ] + $inject); - $users->set($user->id(), $user); - } + $users->set($user->id(), $user); + } - return $users; - } + return $users; + } - /** - * Shortcut for `$users->filter('role', 'admin')` - * - * @param string $role - * @return static - */ - public function role(string $role) - { - return $this->filter('role', $role); - } + /** + * Shortcut for `$users->filter('role', 'admin')` + * + * @param string $role + * @return static + */ + public function role(string $role) + { + return $this->filter('role', $role); + } } diff --git a/kirby/src/Cms/Visitor.php b/kirby/src/Cms/Visitor.php index 44db05c..eae9f3a 100644 --- a/kirby/src/Cms/Visitor.php +++ b/kirby/src/Cms/Visitor.php @@ -15,11 +15,11 @@ use Kirby\Toolkit\Facade; */ class Visitor extends Facade { - /** - * @return \Kirby\Http\Visitor - */ - public static function instance() - { - return App::instance()->visitor(); - } + /** + * @return \Kirby\Http\Visitor + */ + public static function instance() + { + return App::instance()->visitor(); + } } diff --git a/kirby/src/Data/Data.php b/kirby/src/Data/Data.php index 767c972..3c69e32 100644 --- a/kirby/src/Data/Data.php +++ b/kirby/src/Data/Data.php @@ -24,104 +24,104 @@ use Kirby\Filesystem\F; */ class Data { - /** - * Handler Type Aliases - * - * @var array - */ - public static $aliases = [ - 'md' => 'txt', - 'mdown' => 'txt', - 'rss' => 'xml', - 'yml' => 'yaml', - ]; + /** + * Handler Type Aliases + * + * @var array + */ + public static $aliases = [ + 'md' => 'txt', + 'mdown' => 'txt', + 'rss' => 'xml', + 'yml' => 'yaml', + ]; - /** - * All registered handlers - * - * @var array - */ - public static $handlers = [ - 'json' => 'Kirby\Data\Json', - 'php' => 'Kirby\Data\PHP', - 'txt' => 'Kirby\Data\Txt', - 'xml' => 'Kirby\Data\Xml', - 'yaml' => 'Kirby\Data\Yaml', - ]; + /** + * All registered handlers + * + * @var array + */ + public static $handlers = [ + 'json' => 'Kirby\Data\Json', + 'php' => 'Kirby\Data\PHP', + 'txt' => 'Kirby\Data\Txt', + 'xml' => 'Kirby\Data\Xml', + 'yaml' => 'Kirby\Data\Yaml', + ]; - /** - * Handler getter - * - * @param string $type - * @return \Kirby\Data\Handler - */ - public static function handler(string $type) - { - // normalize the type - $type = strtolower($type); + /** + * Handler getter + * + * @param string $type + * @return \Kirby\Data\Handler + */ + public static function handler(string $type) + { + // normalize the type + $type = strtolower($type); - // find a handler or alias - $handler = static::$handlers[$type] ?? - static::$handlers[static::$aliases[$type] ?? null] ?? - null; + // find a handler or alias + $handler = static::$handlers[$type] ?? + static::$handlers[static::$aliases[$type] ?? null] ?? + null; - if ($handler !== null && class_exists($handler)) { - return new $handler(); - } + if ($handler !== null && class_exists($handler)) { + return new $handler(); + } - throw new Exception('Missing handler for type: "' . $type . '"'); - } + throw new Exception('Missing handler for type: "' . $type . '"'); + } - /** - * Decodes data with the specified handler - * - * @param mixed $string - * @param string $type - * @return array - */ - public static function decode($string, string $type): array - { - return static::handler($type)->decode($string); - } + /** + * Decodes data with the specified handler + * + * @param mixed $string + * @param string $type + * @return array + */ + public static function decode($string, string $type): array + { + return static::handler($type)->decode($string); + } - /** - * Encodes data with the specified handler - * - * @param mixed $data - * @param string $type - * @return string - */ - public static function encode($data, string $type): string - { - return static::handler($type)->encode($data); - } + /** + * Encodes data with the specified handler + * + * @param mixed $data + * @param string $type + * @return string + */ + public static function encode($data, string $type): string + { + return static::handler($type)->encode($data); + } - /** - * Reads data from a file; - * the data handler is automatically chosen by - * the extension if not specified - * - * @param string $file - * @param string $type - * @return array - */ - public static function read(string $file, string $type = null): array - { - return static::handler($type ?? F::extension($file))->read($file); - } + /** + * Reads data from a file; + * the data handler is automatically chosen by + * the extension if not specified + * + * @param string $file + * @param string $type + * @return array + */ + public static function read(string $file, string $type = null): array + { + return static::handler($type ?? F::extension($file))->read($file); + } - /** - * Writes data to a file; - * the data handler is automatically chosen by - * the extension if not specified - * - * @param string $file - * @param mixed $data - * @param string $type - * @return bool - */ - public static function write(string $file = null, $data = [], string $type = null): bool - { - return static::handler($type ?? F::extension($file))->write($file, $data); - } + /** + * Writes data to a file; + * the data handler is automatically chosen by + * the extension if not specified + * + * @param string $file + * @param mixed $data + * @param string $type + * @return bool + */ + public static function write(string $file = null, $data = [], string $type = null): bool + { + return static::handler($type ?? F::extension($file))->write($file, $data); + } } diff --git a/kirby/src/Data/Handler.php b/kirby/src/Data/Handler.php index 5b65e24..9c511f8 100644 --- a/kirby/src/Data/Handler.php +++ b/kirby/src/Data/Handler.php @@ -18,49 +18,49 @@ use Kirby\Filesystem\F; */ abstract class Handler { - /** - * Parses an encoded string and returns a multi-dimensional array - * - * Needs to throw an Exception if the file can't be parsed. - * - * @param mixed $string - * @return array - */ - abstract public static function decode($string): array; + /** + * Parses an encoded string and returns a multi-dimensional array + * + * Needs to throw an Exception if the file can't be parsed. + * + * @param mixed $string + * @return array + */ + abstract public static function decode($string): array; - /** - * Converts an array to an encoded string - * - * @param mixed $data - * @return string - */ - abstract public static function encode($data): string; + /** + * Converts an array to an encoded string + * + * @param mixed $data + * @return string + */ + abstract public static function encode($data): string; - /** - * Reads data from a file - * - * @param string $file - * @return array - */ - public static function read(string $file): array - { - $contents = F::read($file); - if ($contents === false) { - throw new Exception('The file "' . $file . '" does not exist'); - } + /** + * Reads data from a file + * + * @param string $file + * @return array + */ + public static function read(string $file): array + { + $contents = F::read($file); + if ($contents === false) { + throw new Exception('The file "' . $file . '" does not exist'); + } - return static::decode($contents); - } + return static::decode($contents); + } - /** - * Writes data to a file - * - * @param string $file - * @param mixed $data - * @return bool - */ - public static function write(string $file = null, $data = []): bool - { - return F::write($file, static::encode($data)); - } + /** + * Writes data to a file + * + * @param string $file + * @param mixed $data + * @return bool + */ + public static function write(string $file = null, $data = []): bool + { + return F::write($file, static::encode($data)); + } } diff --git a/kirby/src/Data/Json.php b/kirby/src/Data/Json.php index 00dba6b..622636b 100644 --- a/kirby/src/Data/Json.php +++ b/kirby/src/Data/Json.php @@ -15,43 +15,43 @@ use Kirby\Exception\InvalidArgumentException; */ class Json extends Handler { - /** - * Converts an array to an encoded JSON string - * - * @param mixed $data - * @return string - */ - public static function encode($data): string - { - return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - } + /** + * Converts an array to an encoded JSON string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } - /** - * Parses an encoded JSON string and returns a multi-dimensional array - * - * @param mixed $string - * @return array - */ - public static function decode($string): array - { - if ($string === null || $string === '') { - return []; - } + /** + * Parses an encoded JSON string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null || $string === '') { + return []; + } - if (is_array($string) === true) { - return $string; - } + if (is_array($string) === true) { + return $string; + } - if (is_string($string) === false) { - throw new InvalidArgumentException('Invalid JSON data; please pass a string'); - } + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid JSON data; please pass a string'); + } - $result = json_decode($string, true); + $result = json_decode($string, true); - if (is_array($result) === true) { - return $result; - } else { - throw new InvalidArgumentException('JSON string is invalid'); - } - } + if (is_array($result) === true) { + return $result; + } else { + throw new InvalidArgumentException('JSON string is invalid'); + } + } } diff --git a/kirby/src/Data/PHP.php b/kirby/src/Data/PHP.php index 0391b29..ff3106c 100644 --- a/kirby/src/Data/PHP.php +++ b/kirby/src/Data/PHP.php @@ -17,78 +17,78 @@ use Kirby\Filesystem\F; */ class PHP extends Handler { - /** - * Converts an array to PHP file content - * - * @param mixed $data - * @param string $indent For internal use only - * @return string - */ - public static function encode($data, string $indent = ''): string - { - switch (gettype($data)) { - case 'array': - $indexed = array_keys($data) === range(0, count($data) - 1); - $array = []; + /** + * Converts an array to PHP file content + * + * @param mixed $data + * @param string $indent For internal use only + * @return string + */ + public static function encode($data, string $indent = ''): string + { + switch (gettype($data)) { + case 'array': + $indexed = array_keys($data) === range(0, count($data) - 1); + $array = []; - foreach ($data as $key => $value) { - $array[] = "$indent " . ($indexed ? '' : static::encode($key) . ' => ') . static::encode($value, "$indent "); - } + foreach ($data as $key => $value) { + $array[] = "$indent " . ($indexed ? '' : static::encode($key) . ' => ') . static::encode($value, "$indent "); + } - return "[\n" . implode(",\n", $array) . "\n" . $indent . ']'; - case 'boolean': - return $data ? 'true' : 'false'; - case 'integer': - case 'double': - return $data; - default: - return var_export($data, true); - } - } + return "[\n" . implode(",\n", $array) . "\n" . $indent . ']'; + case 'boolean': + return $data ? 'true' : 'false'; + case 'integer': + case 'double': + return $data; + default: + return var_export($data, true); + } + } - /** - * PHP strings shouldn't be decoded manually - * - * @param mixed $string - * @return array - */ - public static function decode($string): array - { - throw new BadMethodCallException('The PHP::decode() method is not implemented'); - } + /** + * PHP strings shouldn't be decoded manually + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + throw new BadMethodCallException('The PHP::decode() method is not implemented'); + } - /** - * Reads data from a file - * - * @param string $file - * @return array - */ - public static function read(string $file): array - { - if (is_file($file) !== true) { - throw new Exception('The file "' . $file . '" does not exist'); - } + /** + * Reads data from a file + * + * @param string $file + * @return array + */ + public static function read(string $file): array + { + if (is_file($file) !== true) { + throw new Exception('The file "' . $file . '" does not exist'); + } - return (array)F::load($file, []); - } + return (array)F::load($file, []); + } - /** - * Creates a PHP file with the given data - * - * @param string $file - * @param mixed $data - * @return bool - */ - public static function write(string $file = null, $data = []): bool - { - $php = static::encode($data); - $php = " $value) { - if (empty($key) === true || $value === null) { - continue; - } + foreach (A::wrap($data) as $key => $value) { + if (empty($key) === true || $value === null) { + continue; + } - $key = Str::ucfirst(Str::slug($key)); - $value = static::encodeValue($value); - $result[$key] = static::encodeResult($key, $value); - } + $key = Str::ucfirst(Str::slug($key)); + $value = static::encodeValue($value); + $result[$key] = static::encodeResult($key, $value); + } - return implode("\n\n----\n\n", $result); - } + return implode("\n\n----\n\n", $result); + } - /** - * Helper for converting the value - * - * @param array|string $value - * @return string - */ - protected static function encodeValue($value): string - { - // avoid problems with arrays - if (is_array($value) === true) { - $value = Data::encode($value, 'yaml'); - // avoid problems with localized floats - } elseif (is_float($value) === true) { - $value = Str::float($value); - } + /** + * Helper for converting the value + * + * @param array|string $value + * @return string + */ + protected static function encodeValue($value): string + { + // avoid problems with arrays + if (is_array($value) === true) { + $value = Data::encode($value, 'yaml'); + // avoid problems with localized floats + } elseif (is_float($value) === true) { + $value = Str::float($value); + } - // escape accidental dividers within a field - $value = preg_replace('!(?<=\n|^)----!', '\\----', $value); + // escape accidental dividers within a field + $value = preg_replace('!(?<=\n|^)----!', '\\----', $value); - return $value; - } + return $value; + } - /** - * Helper for converting the key and value to the result string - * - * @param string $key - * @param string $value - * @return string - */ - protected static function encodeResult(string $key, string $value): string - { - $result = $key . ':'; + /** + * Helper for converting the key and value to the result string + * + * @param string $key + * @param string $value + * @return string + */ + protected static function encodeResult(string $key, string $value): string + { + $value = trim($value); + $result = $key . ':'; - // multi-line content - if (preg_match('!\R!', $value) === 1) { - $result .= "\n\n"; - } else { - $result .= ' '; - } + // multi-line content + if (preg_match('!\R!', $value) === 1) { + $result .= "\n\n"; + } else { + $result .= ' '; + } - $result .= trim($value); + $result .= $value; - return $result; - } + return $result; + } - /** - * Parses a Kirby txt string and returns a multi-dimensional array - * - * @param mixed $string - * @return array - */ - public static function decode($string): array - { - if ($string === null || $string === '') { - return []; - } + /** + * Parses a Kirby txt string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null || $string === '') { + return []; + } - if (is_array($string) === true) { - return $string; - } + if (is_array($string) === true) { + return $string; + } - if (is_string($string) === false) { - throw new InvalidArgumentException('Invalid TXT data; please pass a string'); - } + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid TXT data; please pass a string'); + } - // remove BOM - $string = str_replace("\xEF\xBB\xBF", '', $string); - // explode all fields by the line separator - $fields = preg_split('!\n----\s*\n*!', $string); - // start the data array - $data = []; + // remove BOM + $string = str_replace("\xEF\xBB\xBF", '', $string); + // explode all fields by the line separator + $fields = preg_split('!\n----\s*\n*!', $string); + // start the data array + $data = []; - // loop through all fields and add them to the content - foreach ($fields as $field) { - $pos = strpos($field, ':'); - $key = str_replace(['-', ' '], '_', strtolower(trim(substr($field, 0, $pos)))); + // loop through all fields and add them to the content + foreach ($fields as $field) { + $pos = strpos($field, ':'); + $key = str_replace(['-', ' '], '_', strtolower(trim(substr($field, 0, $pos)))); - // Don't add fields with empty keys - if (empty($key) === true) { - continue; - } + // Don't add fields with empty keys + if (empty($key) === true) { + continue; + } - $value = trim(substr($field, $pos + 1)); + $value = trim(substr($field, $pos + 1)); - // unescape escaped dividers within a field - $data[$key] = preg_replace('!(?<=\n|^)\\\\----!', '----', $value); - } + // unescape escaped dividers within a field + $data[$key] = preg_replace('!(?<=\n|^)\\\\----!', '----', $value); + } - return $data; - } + return $data; + } } diff --git a/kirby/src/Data/Xml.php b/kirby/src/Data/Xml.php index 3951df3..ccbd171 100644 --- a/kirby/src/Data/Xml.php +++ b/kirby/src/Data/Xml.php @@ -16,49 +16,49 @@ use Kirby\Toolkit\Xml as XmlConverter; */ class Xml extends Handler { - /** - * Converts an array to an encoded XML string - * - * @param mixed $data - * @return string - */ - public static function encode($data): string - { - return XmlConverter::create($data, 'data'); - } + /** + * Converts an array to an encoded XML string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + return XmlConverter::create($data, 'data'); + } - /** - * Parses an encoded XML string and returns a multi-dimensional array - * - * @param mixed $string - * @return array - */ - public static function decode($string): array - { - if ($string === null || $string === '') { - return []; - } + /** + * Parses an encoded XML string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null || $string === '') { + return []; + } - if (is_array($string) === true) { - return $string; - } + if (is_array($string) === true) { + return $string; + } - if (is_string($string) === false) { - throw new InvalidArgumentException('Invalid XML data; please pass a string'); - } + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid XML data; please pass a string'); + } - $result = XmlConverter::parse($string); + $result = XmlConverter::parse($string); - if (is_array($result) === true) { - // remove the root's name if it is the default to ensure that - // the decoded data is the same as the input to the encode() method - if ($result['@name'] === 'data') { - unset($result['@name']); - } + if (is_array($result) === true) { + // remove the root's name if it is the default to ensure that + // the decoded data is the same as the input to the encode() method + if ($result['@name'] === 'data') { + unset($result['@name']); + } - return $result; - } else { - throw new InvalidArgumentException('XML string is invalid'); - } - } + return $result; + } else { + throw new InvalidArgumentException('XML string is invalid'); + } + } } diff --git a/kirby/src/Data/Yaml.php b/kirby/src/Data/Yaml.php index 205cdde..56c1349 100644 --- a/kirby/src/Data/Yaml.php +++ b/kirby/src/Data/Yaml.php @@ -16,62 +16,62 @@ use Spyc; */ class Yaml extends Handler { - /** - * Converts an array to an encoded YAML string - * - * @param mixed $data - * @return string - */ - public static function encode($data): string - { - // TODO: The locale magic should no longer be - // necessary when support for PHP 7.x is dropped + /** + * Converts an array to an encoded YAML string + * + * @param mixed $data + * @return string + */ + public static function encode($data): string + { + // TODO: The locale magic should no longer be + // necessary when support for PHP 7.x is dropped - // fetch the current locale setting for numbers - $locale = setlocale(LC_NUMERIC, 0); + // fetch the current locale setting for numbers + $locale = setlocale(LC_NUMERIC, 0); - // change to english numerics to avoid issues with floats - setlocale(LC_NUMERIC, 'C'); + // change to english numerics to avoid issues with floats + setlocale(LC_NUMERIC, 'C'); - // $data, $indent, $wordwrap, $no_opening_dashes - $yaml = Spyc::YAMLDump($data, false, false, true); + // $data, $indent, $wordwrap, $no_opening_dashes + $yaml = Spyc::YAMLDump($data, false, false, true); - // restore the previous locale settings - setlocale(LC_NUMERIC, $locale); + // restore the previous locale settings + setlocale(LC_NUMERIC, $locale); - return $yaml; - } + return $yaml; + } - /** - * Parses an encoded YAML string and returns a multi-dimensional array - * - * @param mixed $string - * @return array - */ - public static function decode($string): array - { - if ($string === null || $string === '') { - return []; - } + /** + * Parses an encoded YAML string and returns a multi-dimensional array + * + * @param mixed $string + * @return array + */ + public static function decode($string): array + { + if ($string === null || $string === '') { + return []; + } - if (is_array($string) === true) { - return $string; - } + if (is_array($string) === true) { + return $string; + } - if (is_string($string) === false) { - throw new InvalidArgumentException('Invalid YAML data; please pass a string'); - } + if (is_string($string) === false) { + throw new InvalidArgumentException('Invalid YAML data; please pass a string'); + } - // remove BOM - $string = str_replace("\xEF\xBB\xBF", '', $string); - $result = Spyc::YAMLLoadString($string); + // remove BOM + $string = str_replace("\xEF\xBB\xBF", '', $string); + $result = Spyc::YAMLLoadString($string); - if (is_array($result)) { - return $result; - } else { - // apparently Spyc always returns an array, even for invalid YAML syntax - // so this Exception should currently never be thrown - throw new InvalidArgumentException('The YAML data cannot be parsed'); // @codeCoverageIgnore - } - } + if (is_array($result)) { + return $result; + } else { + // apparently Spyc always returns an array, even for invalid YAML syntax + // so this Exception should currently never be thrown + throw new InvalidArgumentException('The YAML data cannot be parsed'); // @codeCoverageIgnore + } + } } diff --git a/kirby/src/Database/Database.php b/kirby/src/Database/Database.php index d0ddb48..99260e4 100644 --- a/kirby/src/Database/Database.php +++ b/kirby/src/Database/Database.php @@ -20,651 +20,650 @@ use Throwable; */ class Database { - /** - * The number of affected rows for the last query - * - * @var int|null - */ - protected $affected; - - /** - * Whitelist for column names - * - * @var array - */ - protected $columnWhitelist = []; - - /** - * The established connection - * - * @var \PDO|null - */ - protected $connection; - - /** - * A global array of started connections - * - * @var array - */ - public static $connections = []; - - /** - * Database name - * - * @var string - */ - protected $database; - - /** - * @var string - */ - protected $dsn; - - /** - * Set to true to throw exceptions on failed queries - * - * @var bool - */ - protected $fail = false; - - /** - * The connection id - * - * @var string - */ - protected $id; - - /** - * The last error - * - * @var \Exception|null - */ - protected $lastError; - - /** - * The last insert id - * - * @var int|null - */ - protected $lastId; - - /** - * The last query - * - * @var string - */ - protected $lastQuery; - - /** - * The last result set - * - * @var mixed - */ - protected $lastResult; - - /** - * Optional prefix for table names - * - * @var string - */ - protected $prefix; - - /** - * The PDO query statement - * - * @var \PDOStatement|null - */ - protected $statement; - - /** - * List of existing tables in the database - * - * @var array|null - */ - protected $tables; - - /** - * An array with all queries which are being made - * - * @var array - */ - protected $trace = []; - - /** - * The database type (mysql, sqlite) - * - * @var string - */ - protected $type; - - /** - * @var array - */ - public static $types = []; - - /** - * Creates a new Database instance - * - * @param array $params - * @return void - */ - public function __construct(array $params = []) - { - $this->connect($params); - } - - /** - * Returns one of the started instances - * - * @param string|null $id - * @return static|null - */ - public static function instance(string $id = null) - { - return $id === null ? A::last(static::$connections) : static::$connections[$id] ?? null; - } - - /** - * Returns all started instances - * - * @return array - */ - public static function instances(): array - { - return static::$connections; - } - - /** - * Connects to a database - * - * @param array|null $params This can either be a config key or an array of parameters for the connection - * @return \PDO|null - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function connect(array $params = null) - { - $defaults = [ - 'database' => null, - 'type' => 'mysql', - 'prefix' => null, - 'user' => null, - 'password' => null, - 'id' => uniqid() - ]; - - $options = array_merge($defaults, $params); - - // store the database information - $this->database = $options['database']; - $this->type = $options['type']; - $this->prefix = $options['prefix']; - $this->id = $options['id']; - - if (isset(static::$types[$this->type]) === false) { - throw new InvalidArgumentException('Invalid database type: ' . $this->type); - } - - // fetch the dsn and store it - $this->dsn = (static::$types[$this->type]['dsn'])($options); - - // try to connect - $this->connection = new PDO($this->dsn, $options['user'], $options['password']); - $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); - $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); - - // TODO: behavior without this attribute would be preferrable - // (actual types instead of all strings) but would be a breaking change - if ($this->type === 'sqlite') { - $this->connection->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); - } - - // store the connection - static::$connections[$this->id] = $this; - - // return the connection - return $this->connection; - } - - /** - * Returns the currently active connection - * - * @return \PDO|null - */ - public function connection(): ?PDO - { - return $this->connection; - } - - /** - * Sets the exception mode - * - * @param bool $fail - * @return \Kirby\Database\Database - */ - public function fail(bool $fail = true) - { - $this->fail = $fail; - return $this; - } - - /** - * Returns the used database type - * - * @return string - */ - public function type(): string - { - return $this->type; - } - - /** - * Returns the used table name prefix - * - * @return string|null - */ - public function prefix(): ?string - { - return $this->prefix; - } - - /** - * Escapes a value to be used for a safe query - * NOTE: Prepared statements using bound parameters are more secure and solid - * - * @param string $value - * @return string - */ - public function escape(string $value): string - { - return substr($this->connection()->quote($value), 1, -1); - } - - /** - * Adds a value to the db trace and also returns the entire trace if nothing is specified - * - * @param array|null $data - * @return array - */ - public function trace(array $data = null): array - { - // return the full trace - if ($data === null) { - return $this->trace; - } - - // add a new entry to the trace - $this->trace[] = $data; - - return $this->trace; - } - - /** - * Returns the number of affected rows for the last query - * - * @return int|null - */ - public function affected(): ?int - { - return $this->affected; - } - - /** - * Returns the last id if available - * - * @return int|null - */ - public function lastId(): ?int - { - return $this->lastId; - } - - /** - * Returns the last query - * - * @return string|null - */ - public function lastQuery(): ?string - { - return $this->lastQuery; - } - - /** - * Returns the last set of results - * - * @return mixed - */ - public function lastResult() - { - return $this->lastResult; - } - - /** - * Returns the last db error - * - * @return \Throwable - */ - public function lastError() - { - return $this->lastError; - } - - /** - * Returns the name of the database - * - * @return string|null - */ - public function name(): ?string - { - return $this->database; - } - - /** - * Private method to execute database queries. - * This is used by the query() and execute() methods - * - * @param string $query - * @param array $bindings - * @return bool - */ - protected function hit(string $query, array $bindings = []): bool - { - // try to prepare and execute the sql - try { - $this->statement = $this->connection->prepare($query); - $this->statement->execute($bindings); - - $this->affected = $this->statement->rowCount(); - $this->lastId = Str::startsWith($query, 'insert ', true) ? $this->connection->lastInsertId() : null; - $this->lastError = null; - - // store the final sql to add it to the trace later - $this->lastQuery = $this->statement->queryString; - } catch (Throwable $e) { - - // store the error - $this->affected = 0; - $this->lastError = $e; - $this->lastId = null; - $this->lastQuery = $query; - - // only throw the extension if failing is allowed - if ($this->fail === true) { - throw $e; - } - } - - // add a new entry to the singleton trace array - $this->trace([ - 'query' => $this->lastQuery, - 'bindings' => $bindings, - 'error' => $this->lastError - ]); - - // return true or false on success or failure - return $this->lastError === null; - } - - /** - * Executes a sql query, which is expected to return a set of results - * - * @param string $query - * @param array $bindings - * @param array $params - * @return mixed - */ - public function query(string $query, array $bindings = [], array $params = []) - { - $defaults = [ - 'flag' => null, - 'method' => 'fetchAll', - 'fetch' => 'Kirby\Toolkit\Obj', - 'iterator' => 'Kirby\Toolkit\Collection', - ]; - - $options = array_merge($defaults, $params); - - if ($this->hit($query, $bindings) === false) { - return false; - } - - // define the default flag for the fetch method - if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') { - $flags = PDO::FETCH_ASSOC; - } else { - $flags = PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE; - } - - // add optional flags - if (empty($options['flag']) === false) { - $flags |= $options['flag']; - } - - // set the fetch mode - if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') { - $this->statement->setFetchMode($flags); - } else { - $this->statement->setFetchMode($flags, $options['fetch']); - } - - // fetch that stuff - $results = $this->statement->{$options['method']}(); - - // apply the fetch closure to all results if given - if ($options['fetch'] instanceof Closure) { - foreach ($results as $key => $result) { - $results[$key] = $options['fetch']($result, $key); - } - } - - if ($options['iterator'] === 'array') { - return $this->lastResult = $results; - } - - return $this->lastResult = new $options['iterator']($results); - } - - /** - * Executes a sql query, which is expected to not return a set of results - * - * @param string $query - * @param array $bindings - * @return bool - */ - public function execute(string $query, array $bindings = []): bool - { - return $this->lastResult = $this->hit($query, $bindings); - } - - /** - * Returns the correct Sql generator instance - * for the type of database - * - * @return \Kirby\Database\Sql - */ - public function sql() - { - $className = static::$types[$this->type]['sql'] ?? 'Sql'; - return new $className($this); - } - - /** - * Sets the current table, which should be queried. Returns a - * Query object, which can be used to build a full query - * for that table - * - * @param string $table - * @return \Kirby\Database\Query - */ - public function table(string $table) - { - return new Query($this, $this->prefix() . $table); - } - - /** - * Checks if a table exists in the current database - * - * @param string $table - * @return bool - */ - public function validateTable(string $table): bool - { - if ($this->tables === null) { - // Get the list of tables from the database - $sql = $this->sql()->tables(); - $results = $this->query($sql['query'], $sql['bindings']); - - if ($results) { - $this->tables = $results->pluck('name'); - } else { - return false; - } - } - - return in_array($table, $this->tables) === true; - } - - /** - * Checks if a column exists in a specified table - * - * @param string $table - * @param string $column - * @return bool - */ - public function validateColumn(string $table, string $column): bool - { - if (isset($this->columnWhitelist[$table]) === false) { - if ($this->validateTable($table) === false) { - $this->columnWhitelist[$table] = []; - return false; - } - - // Get the column whitelist from the database - $sql = $this->sql()->columns($table); - $results = $this->query($sql['query'], $sql['bindings']); - - if ($results) { - $this->columnWhitelist[$table] = $results->pluck('name'); - } else { - return false; - } - } - - return in_array($column, $this->columnWhitelist[$table]) === true; - } - - /** - * Creates a new table - * - * @param string $table - * @param array $columns - * @return bool - */ - public function createTable($table, $columns = []): bool - { - $sql = $this->sql()->createTable($table, $columns); - $queries = Str::split($sql['query'], ';'); - - foreach ($queries as $query) { - $query = trim($query); - - if ($this->execute($query, $sql['bindings']) === false) { - return false; - } - } - - // update cache - if (in_array($table, $this->tables ?? []) !== true) { - $this->tables[] = $table; - } - - return true; - } - - /** - * Drops a table - * - * @param string $table - * @return bool - */ - public function dropTable(string $table): bool - { - $sql = $this->sql()->dropTable($table); - if ($this->execute($sql['query'], $sql['bindings']) !== true) { - return false; - } - - // update cache - $key = array_search($table, $this->tables ?? []); - if ($key !== false) { - unset($this->tables[$key]); - } - - return true; - } - - /** - * Magic way to start queries for tables by - * using a method named like the table. - * I.e. $db->users()->all() - * - * @param mixed $method - * @param mixed $arguments - * @return \Kirby\Database\Query - */ - public function __call($method, $arguments = null) - { - return $this->table($method); - } + /** + * The number of affected rows for the last query + * + * @var int|null + */ + protected $affected; + + /** + * Whitelist for column names + * + * @var array + */ + protected $columnWhitelist = []; + + /** + * The established connection + * + * @var \PDO|null + */ + protected $connection; + + /** + * A global array of started connections + * + * @var array + */ + public static $connections = []; + + /** + * Database name + * + * @var string + */ + protected $database; + + /** + * @var string + */ + protected $dsn; + + /** + * Set to true to throw exceptions on failed queries + * + * @var bool + */ + protected $fail = false; + + /** + * The connection id + * + * @var string + */ + protected $id; + + /** + * The last error + * + * @var \Exception|null + */ + protected $lastError; + + /** + * The last insert id + * + * @var int|null + */ + protected $lastId; + + /** + * The last query + * + * @var string + */ + protected $lastQuery; + + /** + * The last result set + * + * @var mixed + */ + protected $lastResult; + + /** + * Optional prefix for table names + * + * @var string + */ + protected $prefix; + + /** + * The PDO query statement + * + * @var \PDOStatement|null + */ + protected $statement; + + /** + * List of existing tables in the database + * + * @var array|null + */ + protected $tables; + + /** + * An array with all queries which are being made + * + * @var array + */ + protected $trace = []; + + /** + * The database type (mysql, sqlite) + * + * @var string + */ + protected $type; + + /** + * @var array + */ + public static $types = []; + + /** + * Creates a new Database instance + * + * @param array $params + * @return void + */ + public function __construct(array $params = []) + { + $this->connect($params); + } + + /** + * Returns one of the started instances + * + * @param string|null $id + * @return static|null + */ + public static function instance(string $id = null) + { + return $id === null ? A::last(static::$connections) : static::$connections[$id] ?? null; + } + + /** + * Returns all started instances + * + * @return array + */ + public static function instances(): array + { + return static::$connections; + } + + /** + * Connects to a database + * + * @param array|null $params This can either be a config key or an array of parameters for the connection + * @return \PDO|null + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function connect(array $params = null) + { + $defaults = [ + 'database' => null, + 'type' => 'mysql', + 'prefix' => null, + 'user' => null, + 'password' => null, + 'id' => uniqid() + ]; + + $options = array_merge($defaults, $params); + + // store the database information + $this->database = $options['database']; + $this->type = $options['type']; + $this->prefix = $options['prefix']; + $this->id = $options['id']; + + if (isset(static::$types[$this->type]) === false) { + throw new InvalidArgumentException('Invalid database type: ' . $this->type); + } + + // fetch the dsn and store it + $this->dsn = (static::$types[$this->type]['dsn'])($options); + + // try to connect + $this->connection = new PDO($this->dsn, $options['user'], $options['password']); + $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + $this->connection->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); + + // TODO: behavior without this attribute would be preferrable + // (actual types instead of all strings) but would be a breaking change + if ($this->type === 'sqlite') { + $this->connection->setAttribute(PDO::ATTR_STRINGIFY_FETCHES, true); + } + + // store the connection + static::$connections[$this->id] = $this; + + // return the connection + return $this->connection; + } + + /** + * Returns the currently active connection + * + * @return \PDO|null + */ + public function connection(): ?PDO + { + return $this->connection; + } + + /** + * Sets the exception mode + * + * @param bool $fail + * @return \Kirby\Database\Database + */ + public function fail(bool $fail = true) + { + $this->fail = $fail; + return $this; + } + + /** + * Returns the used database type + * + * @return string + */ + public function type(): string + { + return $this->type; + } + + /** + * Returns the used table name prefix + * + * @return string|null + */ + public function prefix(): ?string + { + return $this->prefix; + } + + /** + * Escapes a value to be used for a safe query + * NOTE: Prepared statements using bound parameters are more secure and solid + * + * @param string $value + * @return string + */ + public function escape(string $value): string + { + return substr($this->connection()->quote($value), 1, -1); + } + + /** + * Adds a value to the db trace and also returns the entire trace if nothing is specified + * + * @param array|null $data + * @return array + */ + public function trace(array $data = null): array + { + // return the full trace + if ($data === null) { + return $this->trace; + } + + // add a new entry to the trace + $this->trace[] = $data; + + return $this->trace; + } + + /** + * Returns the number of affected rows for the last query + * + * @return int|null + */ + public function affected(): ?int + { + return $this->affected; + } + + /** + * Returns the last id if available + * + * @return int|null + */ + public function lastId(): ?int + { + return $this->lastId; + } + + /** + * Returns the last query + * + * @return string|null + */ + public function lastQuery(): ?string + { + return $this->lastQuery; + } + + /** + * Returns the last set of results + * + * @return mixed + */ + public function lastResult() + { + return $this->lastResult; + } + + /** + * Returns the last db error + * + * @return \Throwable + */ + public function lastError() + { + return $this->lastError; + } + + /** + * Returns the name of the database + * + * @return string|null + */ + public function name(): ?string + { + return $this->database; + } + + /** + * Private method to execute database queries. + * This is used by the query() and execute() methods + * + * @param string $query + * @param array $bindings + * @return bool + */ + protected function hit(string $query, array $bindings = []): bool + { + // try to prepare and execute the sql + try { + $this->statement = $this->connection->prepare($query); + $this->statement->execute($bindings); + + $this->affected = $this->statement->rowCount(); + $this->lastId = Str::startsWith($query, 'insert ', true) ? $this->connection->lastInsertId() : null; + $this->lastError = null; + + // store the final sql to add it to the trace later + $this->lastQuery = $this->statement->queryString; + } catch (Throwable $e) { + // store the error + $this->affected = 0; + $this->lastError = $e; + $this->lastId = null; + $this->lastQuery = $query; + + // only throw the extension if failing is allowed + if ($this->fail === true) { + throw $e; + } + } + + // add a new entry to the singleton trace array + $this->trace([ + 'query' => $this->lastQuery, + 'bindings' => $bindings, + 'error' => $this->lastError + ]); + + // return true or false on success or failure + return $this->lastError === null; + } + + /** + * Executes a sql query, which is expected to return a set of results + * + * @param string $query + * @param array $bindings + * @param array $params + * @return mixed + */ + public function query(string $query, array $bindings = [], array $params = []) + { + $defaults = [ + 'flag' => null, + 'method' => 'fetchAll', + 'fetch' => 'Kirby\Toolkit\Obj', + 'iterator' => 'Kirby\Toolkit\Collection', + ]; + + $options = array_merge($defaults, $params); + + if ($this->hit($query, $bindings) === false) { + return false; + } + + // define the default flag for the fetch method + if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') { + $flags = PDO::FETCH_ASSOC; + } else { + $flags = PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE; + } + + // add optional flags + if (empty($options['flag']) === false) { + $flags |= $options['flag']; + } + + // set the fetch mode + if ($options['fetch'] instanceof Closure || $options['fetch'] === 'array') { + $this->statement->setFetchMode($flags); + } else { + $this->statement->setFetchMode($flags, $options['fetch']); + } + + // fetch that stuff + $results = $this->statement->{$options['method']}(); + + // apply the fetch closure to all results if given + if ($options['fetch'] instanceof Closure) { + foreach ($results as $key => $result) { + $results[$key] = $options['fetch']($result, $key); + } + } + + if ($options['iterator'] === 'array') { + return $this->lastResult = $results; + } + + return $this->lastResult = new $options['iterator']($results); + } + + /** + * Executes a sql query, which is expected to not return a set of results + * + * @param string $query + * @param array $bindings + * @return bool + */ + public function execute(string $query, array $bindings = []): bool + { + return $this->lastResult = $this->hit($query, $bindings); + } + + /** + * Returns the correct Sql generator instance + * for the type of database + * + * @return \Kirby\Database\Sql + */ + public function sql() + { + $className = static::$types[$this->type]['sql'] ?? 'Sql'; + return new $className($this); + } + + /** + * Sets the current table, which should be queried. Returns a + * Query object, which can be used to build a full query + * for that table + * + * @param string $table + * @return \Kirby\Database\Query + */ + public function table(string $table) + { + return new Query($this, $this->prefix() . $table); + } + + /** + * Checks if a table exists in the current database + * + * @param string $table + * @return bool + */ + public function validateTable(string $table): bool + { + if ($this->tables === null) { + // Get the list of tables from the database + $sql = $this->sql()->tables(); + $results = $this->query($sql['query'], $sql['bindings']); + + if ($results) { + $this->tables = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($table, $this->tables) === true; + } + + /** + * Checks if a column exists in a specified table + * + * @param string $table + * @param string $column + * @return bool + */ + public function validateColumn(string $table, string $column): bool + { + if (isset($this->columnWhitelist[$table]) === false) { + if ($this->validateTable($table) === false) { + $this->columnWhitelist[$table] = []; + return false; + } + + // Get the column whitelist from the database + $sql = $this->sql()->columns($table); + $results = $this->query($sql['query'], $sql['bindings']); + + if ($results) { + $this->columnWhitelist[$table] = $results->pluck('name'); + } else { + return false; + } + } + + return in_array($column, $this->columnWhitelist[$table]) === true; + } + + /** + * Creates a new table + * + * @param string $table + * @param array $columns + * @return bool + */ + public function createTable($table, $columns = []): bool + { + $sql = $this->sql()->createTable($table, $columns); + $queries = Str::split($sql['query'], ';'); + + foreach ($queries as $query) { + $query = trim($query); + + if ($this->execute($query, $sql['bindings']) === false) { + return false; + } + } + + // update cache + if (in_array($table, $this->tables ?? []) !== true) { + $this->tables[] = $table; + } + + return true; + } + + /** + * Drops a table + * + * @param string $table + * @return bool + */ + public function dropTable(string $table): bool + { + $sql = $this->sql()->dropTable($table); + if ($this->execute($sql['query'], $sql['bindings']) !== true) { + return false; + } + + // update cache + $key = array_search($table, $this->tables ?? []); + if ($key !== false) { + unset($this->tables[$key]); + } + + return true; + } + + /** + * Magic way to start queries for tables by + * using a method named like the table. + * I.e. $db->users()->all() + * + * @param mixed $method + * @param mixed $arguments + * @return \Kirby\Database\Query + */ + public function __call($method, $arguments = null) + { + return $this->table($method); + } } /** * MySQL database connector */ Database::$types['mysql'] = [ - 'sql' => 'Kirby\Database\Sql\Mysql', - 'dsn' => function (array $params) { - if (isset($params['host']) === false && isset($params['socket']) === false) { - throw new InvalidArgumentException('The mysql connection requires either a "host" or a "socket" parameter'); - } + 'sql' => 'Kirby\Database\Sql\Mysql', + 'dsn' => function (array $params) { + if (isset($params['host']) === false && isset($params['socket']) === false) { + throw new InvalidArgumentException('The mysql connection requires either a "host" or a "socket" parameter'); + } - if (isset($params['database']) === false) { - throw new InvalidArgumentException('The mysql connection requires a "database" parameter'); - } + if (isset($params['database']) === false) { + throw new InvalidArgumentException('The mysql connection requires a "database" parameter'); + } - $parts = []; + $parts = []; - if (empty($params['host']) === false) { - $parts[] = 'host=' . $params['host']; - } + if (empty($params['host']) === false) { + $parts[] = 'host=' . $params['host']; + } - if (empty($params['port']) === false) { - $parts[] = 'port=' . $params['port']; - } + if (empty($params['port']) === false) { + $parts[] = 'port=' . $params['port']; + } - if (empty($params['socket']) === false) { - $parts[] = 'unix_socket=' . $params['socket']; - } + if (empty($params['socket']) === false) { + $parts[] = 'unix_socket=' . $params['socket']; + } - if (empty($params['database']) === false) { - $parts[] = 'dbname=' . $params['database']; - } + if (empty($params['database']) === false) { + $parts[] = 'dbname=' . $params['database']; + } - $parts[] = 'charset=' . ($params['charset'] ?? 'utf8'); + $parts[] = 'charset=' . ($params['charset'] ?? 'utf8'); - return 'mysql:' . implode(';', $parts); - } + return 'mysql:' . implode(';', $parts); + } ]; /** * SQLite database connector */ Database::$types['sqlite'] = [ - 'sql' => 'Kirby\Database\Sql\Sqlite', - 'dsn' => function (array $params) { - if (isset($params['database']) === false) { - throw new InvalidArgumentException('The sqlite connection requires a "database" parameter'); - } + 'sql' => 'Kirby\Database\Sql\Sqlite', + 'dsn' => function (array $params) { + if (isset($params['database']) === false) { + throw new InvalidArgumentException('The sqlite connection requires a "database" parameter'); + } - return 'sqlite:' . $params['database']; - } + return 'sqlite:' . $params['database']; + } ]; diff --git a/kirby/src/Database/Db.php b/kirby/src/Database/Db.php index 4686b36..e57562f 100644 --- a/kirby/src/Database/Db.php +++ b/kirby/src/Database/Db.php @@ -16,121 +16,121 @@ use Kirby\Toolkit\Config; */ class Db { - /** - * Query shortcuts - * - * @var array - */ - public static $queries = []; + /** + * Query shortcuts + * + * @var array + */ + public static $queries = []; - /** - * The singleton Database object - * - * @var \Kirby\Database\Database - */ - public static $connection = null; + /** + * The singleton Database object + * + * @var \Kirby\Database\Database + */ + public static $connection = null; - /** - * (Re)connect the database - * - * @param array|null $params Pass `[]` to use the default params from the config, - * don't pass any argument to get the current connection - * @return \Kirby\Database\Database - */ - public static function connect(?array $params = null) - { - if ($params === null && static::$connection !== null) { - return static::$connection; - } + /** + * (Re)connect the database + * + * @param array|null $params Pass `[]` to use the default params from the config, + * don't pass any argument to get the current connection + * @return \Kirby\Database\Database + */ + public static function connect(?array $params = null) + { + if ($params === null && static::$connection !== null) { + return static::$connection; + } - // try to connect with the default - // connection settings if no params are set - $params ??= [ - 'type' => Config::get('db.type', 'mysql'), - 'host' => Config::get('db.host', 'localhost'), - 'user' => Config::get('db.user', 'root'), - 'password' => Config::get('db.password', ''), - 'database' => Config::get('db.database', ''), - 'prefix' => Config::get('db.prefix', ''), - 'port' => Config::get('db.port', '') - ]; + // try to connect with the default + // connection settings if no params are set + $params ??= [ + 'type' => Config::get('db.type', 'mysql'), + 'host' => Config::get('db.host', 'localhost'), + 'user' => Config::get('db.user', 'root'), + 'password' => Config::get('db.password', ''), + 'database' => Config::get('db.database', ''), + 'prefix' => Config::get('db.prefix', ''), + 'port' => Config::get('db.port', '') + ]; - return static::$connection = new Database($params); - } + return static::$connection = new Database($params); + } - /** - * Returns the current database connection - * - * @return \Kirby\Database\Database|null - */ - public static function connection() - { - return static::$connection; - } + /** + * Returns the current database connection + * + * @return \Kirby\Database\Database|null + */ + public static function connection() + { + return static::$connection; + } - /** - * Sets the current table which should be queried. Returns a - * Query object, which can be used to build a full query for - * that table. - * - * @param string $table - * @return \Kirby\Database\Query - */ - public static function table(string $table) - { - $db = static::connect(); - return $db->table($table); - } + /** + * Sets the current table which should be queried. Returns a + * Query object, which can be used to build a full query for + * that table. + * + * @param string $table + * @return \Kirby\Database\Query + */ + public static function table(string $table) + { + $db = static::connect(); + return $db->table($table); + } - /** - * Executes a raw SQL query which expects a set of results - * - * @param string $query - * @param array $bindings - * @param array $params - * @return mixed - */ - public static function query(string $query, array $bindings = [], array $params = []) - { - $db = static::connect(); - return $db->query($query, $bindings, $params); - } + /** + * Executes a raw SQL query which expects a set of results + * + * @param string $query + * @param array $bindings + * @param array $params + * @return mixed + */ + public static function query(string $query, array $bindings = [], array $params = []) + { + $db = static::connect(); + return $db->query($query, $bindings, $params); + } - /** - * Executes a raw SQL query which expects no set of results (i.e. update, insert, delete) - * - * @param string $query - * @param array $bindings - * @return bool - */ - public static function execute(string $query, array $bindings = []): bool - { - $db = static::connect(); - return $db->execute($query, $bindings); - } + /** + * Executes a raw SQL query which expects no set of results (i.e. update, insert, delete) + * + * @param string $query + * @param array $bindings + * @return bool + */ + public static function execute(string $query, array $bindings = []): bool + { + $db = static::connect(); + return $db->execute($query, $bindings); + } - /** - * Magic calls for other static Db methods are - * redirected to either a predefined query or - * the respective method of the Database object - * - * @param string $method - * @param mixed $arguments - * @return mixed - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function __callStatic(string $method, $arguments) - { - if (isset(static::$queries[$method])) { - return (static::$queries[$method])(...$arguments); - } + /** + * Magic calls for other static Db methods are + * redirected to either a predefined query or + * the respective method of the Database object + * + * @param string $method + * @param mixed $arguments + * @return mixed + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function __callStatic(string $method, $arguments) + { + if (isset(static::$queries[$method])) { + return (static::$queries[$method])(...$arguments); + } - if (static::$connection !== null && method_exists(static::$connection, $method) === true) { - return call_user_func_array([static::$connection, $method], $arguments); - } + if (static::$connection !== null && method_exists(static::$connection, $method) === true) { + return call_user_func_array([static::$connection, $method], $arguments); + } - throw new InvalidArgumentException('Invalid static Db method: ' . $method); - } + throw new InvalidArgumentException('Invalid static Db method: ' . $method); + } } // @codeCoverageIgnoreStart @@ -147,7 +147,7 @@ class Db * @return mixed */ Db::$queries['select'] = function (string $table, $columns = '*', $where = null, string $order = null, int $offset = 0, int $limit = null) { - return Db::table($table)->select($columns)->where($where)->order($order)->offset($offset)->limit($limit)->all(); + return Db::table($table)->select($columns)->where($where)->order($order)->offset($offset)->limit($limit)->all(); }; /** @@ -162,7 +162,7 @@ Db::$queries['select'] = function (string $table, $columns = '*', $where = null, * @return mixed */ Db::$queries['first'] = Db::$queries['row'] = Db::$queries['one'] = function (string $table, $columns = '*', $where = null, string $order = null) { - return Db::table($table)->select($columns)->where($where)->order($order)->first(); + return Db::table($table)->select($columns)->where($where)->order($order)->first(); }; /** @@ -177,7 +177,7 @@ Db::$queries['first'] = Db::$queries['row'] = Db::$queries['one'] = function (st * @return mixed */ Db::$queries['column'] = function (string $table, string $column, $where = null, string $order = null, int $offset = 0, int $limit = null) { - return Db::table($table)->where($where)->order($order)->offset($offset)->limit($limit)->column($column); + return Db::table($table)->where($where)->order($order)->offset($offset)->limit($limit)->column($column); }; /** @@ -188,7 +188,7 @@ Db::$queries['column'] = function (string $table, string $column, $where = null, * @return mixed Returns the last inserted id on success or false */ Db::$queries['insert'] = function (string $table, array $values) { - return Db::table($table)->insert($values); + return Db::table($table)->insert($values); }; /** @@ -200,7 +200,7 @@ Db::$queries['insert'] = function (string $table, array $values) { * @return bool */ Db::$queries['update'] = function (string $table, array $values, $where = null): bool { - return Db::table($table)->where($where)->update($values); + return Db::table($table)->where($where)->update($values); }; /** @@ -211,7 +211,7 @@ Db::$queries['update'] = function (string $table, array $values, $where = null): * @return bool */ Db::$queries['delete'] = function (string $table, $where = null): bool { - return Db::table($table)->where($where)->delete(); + return Db::table($table)->where($where)->delete(); }; /** @@ -222,7 +222,7 @@ Db::$queries['delete'] = function (string $table, $where = null): bool { * @return int */ Db::$queries['count'] = function (string $table, $where = null): int { - return Db::table($table)->where($where)->count(); + return Db::table($table)->where($where)->count(); }; /** @@ -234,7 +234,7 @@ Db::$queries['count'] = function (string $table, $where = null): int { * @return float */ Db::$queries['min'] = function (string $table, string $column, $where = null): float { - return Db::table($table)->where($where)->min($column); + return Db::table($table)->where($where)->min($column); }; /** @@ -246,7 +246,7 @@ Db::$queries['min'] = function (string $table, string $column, $where = null): f * @return float */ Db::$queries['max'] = function (string $table, string $column, $where = null): float { - return Db::table($table)->where($where)->max($column); + return Db::table($table)->where($where)->max($column); }; /** @@ -258,7 +258,7 @@ Db::$queries['max'] = function (string $table, string $column, $where = null): f * @return float */ Db::$queries['avg'] = function (string $table, string $column, $where = null): float { - return Db::table($table)->where($where)->avg($column); + return Db::table($table)->where($where)->avg($column); }; /** @@ -270,7 +270,7 @@ Db::$queries['avg'] = function (string $table, string $column, $where = null): f * @return float */ Db::$queries['sum'] = function (string $table, string $column, $where = null): float { - return Db::table($table)->where($where)->sum($column); + return Db::table($table)->where($where)->sum($column); }; // @codeCoverageIgnoreEnd diff --git a/kirby/src/Database/Query.php b/kirby/src/Database/Query.php index 1219333..9354892 100644 --- a/kirby/src/Database/Query.php +++ b/kirby/src/Database/Query.php @@ -19,1047 +19,1050 @@ use Kirby\Toolkit\Str; */ class Query { - public const ERROR_INVALID_QUERY_METHOD = 0; - - /** - * Parent Database object - * - * @var \Kirby\Database\Database - */ - protected $database = null; - - /** - * The object which should be fetched for each row - * or function to call for each row - * - * @var string|\Closure - */ - protected $fetch = 'Kirby\Toolkit\Obj'; - - /** - * The iterator class, which should be used for result sets - * - * @var string - */ - protected $iterator = 'Kirby\Toolkit\Collection'; - - /** - * An array of bindings for the final query - * - * @var array - */ - protected $bindings = []; - - /** - * The table name - * - * @var string - */ - protected $table; - - /** - * The name of the primary key column - * - * @var string - */ - protected $primaryKeyName = 'id'; - - /** - * An array with additional join parameters - * - * @var array - */ - protected $join; - - /** - * A list of columns, which should be selected - * - * @var array|string - */ - protected $select; - - /** - * Boolean for distinct select clauses - * - * @var bool - */ - protected $distinct; - - /** - * Boolean for if exceptions should be thrown on failing queries - * - * @var bool - */ - protected $fail = false; - - /** - * A list of values for update and insert clauses - * - * @var array - */ - protected $values; - - /** - * WHERE clause - * - * @var mixed - */ - protected $where; - - /** - * GROUP BY clause - * - * @var mixed - */ - protected $group; - - /** - * HAVING clause - * - * @var mixed - */ - protected $having; - - /** - * ORDER BY clause - * - * @var mixed - */ - protected $order; - - /** - * The offset, which should be applied to the select query - * - * @var int - */ - protected $offset = 0; - - /** - * The limit, which should be applied to the select query - * - * @var int - */ - protected $limit; - - /** - * Boolean to enable query debugging - * - * @var bool - */ - protected $debug = false; - - /** - * Constructor - * - * @param \Kirby\Database\Database $database Database object - * @param string $table Optional name of the table, which should be queried - */ - public function __construct(Database $database, string $table) - { - $this->database = $database; - $this->table($table); - } - - /** - * Reset the query class after each db hit - */ - protected function reset() - { - $this->bindings = []; - $this->join = null; - $this->select = null; - $this->distinct = null; - $this->fail = false; - $this->values = null; - $this->where = null; - $this->group = null; - $this->having = null; - $this->order = null; - $this->offset = 0; - $this->limit = null; - $this->debug = false; - } - - /** - * Enables query debugging. - * If enabled, the query will return an array with all important info about - * the query instead of actually executing the query and returning results - * - * @param bool $debug - * @return \Kirby\Database\Query - */ - public function debug(bool $debug = true) - { - $this->debug = $debug; - return $this; - } - - /** - * Enables distinct select clauses. - * - * @param bool $distinct - * @return \Kirby\Database\Query - */ - public function distinct(bool $distinct = true) - { - $this->distinct = $distinct; - return $this; - } - - /** - * Enables failing queries. - * If enabled queries will no longer fail silently but throw an exception - * - * @param bool $fail - * @return \Kirby\Database\Query - */ - public function fail(bool $fail = true) - { - $this->fail = $fail; - return $this; - } - - /** - * Sets the object class, which should be fetched; - * set this to `'array'` to get a simple array instead of an object; - * pass a function that receives the `$data` and the `$key` to generate arbitrary data structures - * - * @param string|\Closure $fetch - * @return \Kirby\Database\Query - */ - public function fetch($fetch) - { - $this->fetch = $fetch; - return $this; - } - - /** - * Sets the iterator class, which should be used for multiple results - * Set this to array to get a simple array instead of an iterator object - * - * @param string $iterator - * @return \Kirby\Database\Query - */ - public function iterator(string $iterator) - { - $this->iterator = $iterator; - return $this; - } - - /** - * Sets the name of the table, which should be queried - * - * @param string $table - * @return \Kirby\Database\Query - * @throws \Kirby\Exception\InvalidArgumentException if the table does not exist - */ - public function table(string $table) - { - if ($this->database->validateTable($table) === false) { - throw new InvalidArgumentException('Invalid table: ' . $table); - } - - $this->table = $table; - return $this; - } - - /** - * Sets the name of the primary key column - * - * @param string $primaryKeyName - * @return \Kirby\Database\Query - */ - public function primaryKeyName(string $primaryKeyName) - { - $this->primaryKeyName = $primaryKeyName; - return $this; - } - - /** - * Sets the columns, which should be selected from the table - * By default all columns will be selected - * - * @param mixed $select Pass either a string of columns or an array - * @return \Kirby\Database\Query - */ - public function select($select) - { - $this->select = $select; - return $this; - } - - /** - * Adds a new join clause to the query - * - * @param string $table Name of the table, which should be joined - * @param string $on The on clause for this join - * @param string $type The join type. Uses an inner join by default - * @return $this - */ - public function join(string $table, string $on, string $type = 'JOIN') - { - $join = [ - 'table' => $table, - 'on' => $on, - 'type' => $type - ]; - - $this->join[] = $join; - return $this; - } - - /** - * Shortcut for creating a left join clause - * - * @param string $table Name of the table, which should be joined - * @param string $on The on clause for this join - * @return \Kirby\Database\Query - */ - public function leftJoin(string $table, string $on) - { - return $this->join($table, $on, 'left'); - } - - /** - * Shortcut for creating a right join clause - * - * @param string $table Name of the table, which should be joined - * @param string $on The on clause for this join - * @return \Kirby\Database\Query - */ - public function rightJoin(string $table, string $on) - { - return $this->join($table, $on, 'right'); - } - - /** - * Shortcut for creating an inner join clause - * - * @param string $table Name of the table, which should be joined - * @param string $on The on clause for this join - * @return \Kirby\Database\Query - */ - public function innerJoin($table, $on) - { - return $this->join($table, $on, 'inner join'); - } - - /** - * Sets the values which should be used for the update or insert clause - * - * @param mixed $values Can either be a string or an array of values - * @return \Kirby\Database\Query - */ - public function values($values = []) - { - if ($values !== null) { - $this->values = $values; - } - return $this; - } - - /** - * Attaches additional bindings to the query. - * Also can be used as getter for all attached bindings by not passing an argument. - * - * @param mixed $bindings Array of bindings or null to use this method as getter - * @return array|\Kirby\Database\Query - */ - public function bindings(array $bindings = null) - { - if (is_array($bindings) === true) { - $this->bindings = array_merge($this->bindings, $bindings); - return $this; - } - - return $this->bindings; - } - - /** - * Attaches an additional where clause - * - * All available ways to add where clauses - * - * ->where('username like "myuser"'); (args: 1) - * ->where(['username' => 'myuser']); (args: 1) - * ->where(function($where) { $where->where('id', '=', 1) }) (args: 1) - * ->where('username like ?', 'myuser') (args: 2) - * ->where('username', 'like', 'myuser'); (args: 3) - * - * @param mixed ...$args - * @return \Kirby\Database\Query - */ - public function where(...$args) - { - $this->where = $this->filterQuery($args, $this->where); - return $this; - } - - /** - * Shortcut to attach a where clause with an OR operator. - * Check out the where() method docs for additional info. - * - * @param mixed ...$args - * @return \Kirby\Database\Query - */ - public function orWhere(...$args) - { - $mode = A::last($args); - - // if there's a where clause mode attribute attached… - if (in_array($mode, ['AND', 'OR']) === true) { - // remove that from the list of arguments - array_pop($args); - } - - // make sure to always attach the OR mode indicator - $args[] = 'OR'; - - $this->where(...$args); - return $this; - } - - /** - * Shortcut to attach a where clause with an AND operator. - * Check out the where() method docs for additional info. - * - * @param mixed ...$args - * @return \Kirby\Database\Query - */ - public function andWhere(...$args) - { - $mode = A::last($args); - - // if there's a where clause mode attribute attached… - if (in_array($mode, ['AND', 'OR']) === true) { - // remove that from the list of arguments - array_pop($args); - } - - // make sure to always attach the AND mode indicator - $args[] = 'AND'; - - $this->where(...$args); - return $this; - } - - /** - * Attaches a group by clause - * - * @param string|null $group - * @return \Kirby\Database\Query - */ - public function group(string $group = null) - { - $this->group = $group; - return $this; - } - - /** - * Attaches an additional having clause - * - * All available ways to add having clauses - * - * ->having('username like "myuser"'); (args: 1) - * ->having(['username' => 'myuser']); (args: 1) - * ->having(function($having) { $having->having('id', '=', 1) }) (args: 1) - * ->having('username like ?', 'myuser') (args: 2) - * ->having('username', 'like', 'myuser'); (args: 3) - * - * @param mixed ...$args - * @return \Kirby\Database\Query - */ - public function having(...$args) - { - $this->having = $this->filterQuery($args, $this->having); - return $this; - } - - /** - * Attaches an order clause - * - * @param string|null $order - * @return \Kirby\Database\Query - */ - public function order(string $order = null) - { - $this->order = $order; - return $this; - } - - /** - * Sets the offset for select clauses - * - * @param int|null $offset - * @return \Kirby\Database\Query - */ - public function offset(int $offset = null) - { - $this->offset = $offset; - return $this; - } - - /** - * Sets the limit for select clauses - * - * @param int|null $limit - * @return \Kirby\Database\Query - */ - public function limit(int $limit = null) - { - $this->limit = $limit; - return $this; - } - - /** - * Builds the different types of SQL queries - * This uses the SQL class to build stuff. - * - * @param string $type (select, update, insert) - * @return array The final query - */ - public function build(string $type) - { - $sql = $this->database->sql(); - - switch ($type) { - case 'select': - return $sql->select([ - 'table' => $this->table, - 'columns' => $this->select, - 'join' => $this->join, - 'distinct' => $this->distinct, - 'where' => $this->where, - 'group' => $this->group, - 'having' => $this->having, - 'order' => $this->order, - 'offset' => $this->offset, - 'limit' => $this->limit, - 'bindings' => $this->bindings - ]); - case 'update': - return $sql->update([ - 'table' => $this->table, - 'where' => $this->where, - 'values' => $this->values, - 'bindings' => $this->bindings - ]); - case 'insert': - return $sql->insert([ - 'table' => $this->table, - 'values' => $this->values, - 'bindings' => $this->bindings - ]); - case 'delete': - return $sql->delete([ - 'table' => $this->table, - 'where' => $this->where, - 'bindings' => $this->bindings - ]); - } - } - - /** - * Builds a count query - * - * @return int - */ - public function count(): int - { - return (int)$this->aggregate('COUNT'); - } - - /** - * Builds a max query - * - * @param string $column - * @return float - */ - public function max(string $column): float - { - return (float)$this->aggregate('MAX', $column); - } - - /** - * Builds a min query - * - * @param string $column - * @return float - */ - public function min(string $column): float - { - return (float)$this->aggregate('MIN', $column); - } - - /** - * Builds a sum query - * - * @param string $column - * @return float - */ - public function sum(string $column): float - { - return (float)$this->aggregate('SUM', $column); - } - - /** - * Builds an average query - * - * @param string $column - * @return float - */ - public function avg(string $column): float - { - return (float)$this->aggregate('AVG', $column); - } - - /** - * Builds an aggregation query. - * This is used by all the aggregation methods above - * - * @param string $method - * @param string $column - * @param int $default An optional default value, which should be returned if the query fails - * @return mixed - */ - public function aggregate(string $method, string $column = '*', $default = 0) - { - // reset the sorting to avoid counting issues - $this->order = null; - - // validate column - if ($column !== '*') { - $sql = $this->database->sql(); - $column = $sql->columnName($this->table, $column); - } - - $fetch = $this->fetch; - $row = $this->select($method . '(' . $column . ') as aggregation')->fetch('Obj')->first(); - - if ($this->debug === true) { - return $row; - } - - $result = $row ? $row->get('aggregation') : $default; - - $this->fetch($fetch); - - return $result; - } - - /** - * Used as an internal shortcut for firing a db query - * - * @param string|array $sql - * @param array $params - * @return mixed - */ - protected function query($sql, array $params = []) - { - if (is_string($sql) === true) { - $sql = [ - 'query' => $sql, - 'bindings' => $this->bindings() - ]; - } - - if ($this->debug) { - return [ - 'query' => $sql['query'], - 'bindings' => $this->bindings(), - 'options' => $params - ]; - } - - if ($this->fail) { - $this->database->fail(); - } - - $result = $this->database->query($sql['query'], $sql['bindings'], $params); - - $this->reset(); - - return $result; - } - - /** - * Used as an internal shortcut for executing a db query - * - * @param string|array $sql - * @param array $params - * @return mixed - */ - protected function execute($sql, array $params = []) - { - if (is_string($sql) === true) { - $sql = [ - 'query' => $sql, - 'bindings' => $this->bindings() - ]; - } - - if ($this->debug === true) { - return [ - 'query' => $sql['query'], - 'bindings' => $sql['bindings'], - 'options' => $params - ]; - } - - if ($this->fail) { - $this->database->fail(); - } - - $result = $this->database->execute($sql['query'], $sql['bindings']); - - $this->reset(); - - return $result; - } - - /** - * Selects only one row from a table - * - * @return object - */ - public function first() - { - return $this->query($this->offset(0)->limit(1)->build('select'), [ - 'fetch' => $this->fetch, - 'iterator' => 'array', - 'method' => 'fetch', - ]); - } - - /** - * Selects only one row from a table - * - * @return object - */ - public function row() - { - return $this->first(); - } - - /** - * Selects only one row from a table - * - * @return object - */ - public function one() - { - return $this->first(); - } - - /** - * Automatically adds pagination to a query - * - * @param int $page - * @param int $limit The number of rows, which should be returned for each page - * @return object Collection iterator with attached pagination object - */ - public function page(int $page, int $limit) - { - // clone this to create a counter query - $counter = clone $this; - - // count the total number of rows for this query - $count = $counter->debug(false)->count(); - - // pagination - $pagination = new Pagination([ - 'limit' => $limit, - 'page' => $page, - 'total' => $count, - ]); - - // apply it to the dataset and retrieve all rows. make sure to use Collection as the iterator to be able to attach the pagination object - $iterator = $this->iterator; - $collection = $this->offset($pagination->offset())->limit($pagination->limit())->iterator('Collection')->all(); - - $this->iterator($iterator); - - // return debug information if debug mode is active - if ($this->debug) { - $collection['totalcount'] = $count; - return $collection; - } - - // store all pagination vars in a separate object - if ($collection) { - $collection->paginate($pagination); - } - - // return the limited collection - return $collection; - } - - /** - * Returns all matching rows from a table - * - * @return mixed - */ - public function all() - { - return $this->query($this->build('select'), [ - 'fetch' => $this->fetch, - 'iterator' => $this->iterator, - ]); - } - - /** - * Returns only values from a single column - * - * @param string $column - * @return mixed - */ - public function column(string $column) - { - // if there isn't already an explicit order, order by the primary key - // instead of the column that was requested (which would be implied otherwise) - if ($this->order === null) { - $sql = $this->database->sql(); - $primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName); - - $this->order($primaryKey . ' ASC'); - } - - $results = $this->query($this->select([$column])->build('select'), [ - 'iterator' => 'array', - 'fetch' => 'array', - ]); - - if ($this->debug === true) { - return $results; - } - - $results = array_column($results, $column); - - if ($this->iterator === 'array') { - return $results; - } - - $iterator = $this->iterator; - - return new $iterator($results); - } - - /** - * Find a single row by column and value - * - * @param string $column - * @param mixed $value - * @return mixed - */ - public function findBy(string $column, $value) - { - return $this->where([$column => $value])->first(); - } - - /** - * Find a single row by its primary key - * - * @param mixed $id - * @return mixed - */ - public function find($id) - { - return $this->findBy($this->primaryKeyName, $id); - } - - /** - * Fires an insert query - * - * @param mixed $values You can pass values here or set them with ->values() before - * @return mixed Returns the last inserted id on success or false. - */ - public function insert($values = null) - { - $query = $this->execute($this->values($values)->build('insert')); - - if ($this->debug === true) { - return $query; - } - - return $query ? $this->database->lastId() : false; - } - - /** - * Fires an update query - * - * @param mixed $values You can pass values here or set them with ->values() before - * @param mixed $where You can pass a where clause here or set it with ->where() before - * @return bool - */ - public function update($values = null, $where = null) - { - return $this->execute($this->values($values)->where($where)->build('update')); - } - - /** - * Fires a delete query - * - * @param mixed $where You can pass a where clause here or set it with ->where() before - * @return bool - */ - public function delete($where = null) - { - return $this->execute($this->where($where)->build('delete')); - } - - /** - * Enables magic queries like findByUsername or findByEmail - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - if (preg_match('!^findBy([a-z]+)!i', $method, $match)) { - $column = Str::lower($match[1]); - return $this->findBy($column, $arguments[0]); - } else { - throw new InvalidArgumentException('Invalid query method: ' . $method, static::ERROR_INVALID_QUERY_METHOD); - } - } - - /** - * Builder for where and having clauses - * - * @param array $args Arguments, see where() description - * @param mixed $current Current value (like $this->where) - * @return string - */ - protected function filterQuery(array $args, $current) - { - $mode = A::last($args); - $result = ''; - - // if there's a where clause mode attribute attached… - if (in_array($mode, ['AND', 'OR'])) { - // remove that from the list of arguments - array_pop($args); - } else { - $mode = 'AND'; - } - - switch (count($args)) { - case 1: - - if ($args[0] === null) { - return $current; - - // ->where('username like "myuser"'); - } elseif (is_string($args[0]) === true) { - - // simply add the entire string to the where clause - // escaping or using bindings has to be done before calling this method - $result = $args[0]; - - // ->where(['username' => 'myuser']); - } elseif (is_array($args[0]) === true) { - - // simple array mode (AND operator) - $sql = $this->database->sql()->values($this->table, $args[0], ' AND ', true, true); - - $result = $sql['query']; - - $this->bindings($sql['bindings']); - } elseif (is_callable($args[0]) === true) { - $query = clone $this; - call_user_func($args[0], $query); - - // copy over the bindings from the nested query - $this->bindings = array_merge($this->bindings, $query->bindings); - - $result = '(' . $query->where . ')'; - } - - break; - case 2: - - // ->where('username like :username', ['username' => 'myuser']) - if (is_string($args[0]) === true && is_array($args[1]) === true) { - - // prepared where clause - $result = $args[0]; - - // store the bindings - $this->bindings($args[1]); - - // ->where('username like ?', 'myuser') - } elseif (is_string($args[0]) === true && is_string($args[1]) === true) { - - // prepared where clause - $result = $args[0]; - - // store the bindings - $this->bindings([$args[1]]); - } - - break; - case 3: - - // ->where('username', 'like', 'myuser'); - if (is_string($args[0]) === true && is_string($args[1]) === true) { - - // validate column - $sql = $this->database->sql(); - $key = $sql->columnName($this->table, $args[0]); - - // ->where('username', 'in', ['myuser', 'myotheruser']); - $predicate = trim(strtoupper($args[1])); - if (is_array($args[2]) === true) { - if (in_array($predicate, ['IN', 'NOT IN']) === false) { - throw new InvalidArgumentException('Invalid predicate ' . $predicate); - } - - // build a list of bound values - $values = []; - $bindings = []; - - foreach ($args[2] as $value) { - $valueBinding = $sql->bindingName('value'); - $bindings[$valueBinding] = $value; - $values[] = $valueBinding; - } - - // add that to the where clause in parenthesis - $result = $key . ' ' . $predicate . ' (' . implode(', ', $values) . ')'; - - // ->where('username', 'like', 'myuser'); - } else { - $predicates = [ - '=', '>=', '>', '<=', '<', '<>', '!=', '<=>', - 'IS', 'IS NOT', - 'BETWEEN', 'NOT BETWEEN', - 'LIKE', 'NOT LIKE', - 'SOUNDS LIKE', - 'REGEXP', 'NOT REGEXP' - ]; - - if (in_array($predicate, $predicates) === false) { - throw new InvalidArgumentException('Invalid predicate/operator ' . $predicate); - } - - $valueBinding = $sql->bindingName('value'); - $bindings[$valueBinding] = $args[2]; - - $result = $key . ' ' . $predicate . ' ' . $valueBinding; - } - $this->bindings($bindings); - } - - break; - - } - - // attach the where clause - if (empty($current) === false) { - return $current . ' ' . $mode . ' ' . $result; - } else { - return $result; - } - } + public const ERROR_INVALID_QUERY_METHOD = 0; + + /** + * Parent Database object + * + * @var \Kirby\Database\Database + */ + protected $database = null; + + /** + * The object which should be fetched for each row + * or function to call for each row + * + * @var string|\Closure + */ + protected $fetch = 'Kirby\Toolkit\Obj'; + + /** + * The iterator class, which should be used for result sets + * + * @var string + */ + protected $iterator = 'Kirby\Toolkit\Collection'; + + /** + * An array of bindings for the final query + * + * @var array + */ + protected $bindings = []; + + /** + * The table name + * + * @var string + */ + protected $table; + + /** + * The name of the primary key column + * + * @var string + */ + protected $primaryKeyName = 'id'; + + /** + * An array with additional join parameters + * + * @var array + */ + protected $join; + + /** + * A list of columns, which should be selected + * + * @var array|string + */ + protected $select; + + /** + * Boolean for distinct select clauses + * + * @var bool + */ + protected $distinct; + + /** + * Boolean for if exceptions should be thrown on failing queries + * + * @var bool + */ + protected $fail = false; + + /** + * A list of values for update and insert clauses + * + * @var array + */ + protected $values; + + /** + * WHERE clause + * + * @var mixed + */ + protected $where; + + /** + * GROUP BY clause + * + * @var mixed + */ + protected $group; + + /** + * HAVING clause + * + * @var mixed + */ + protected $having; + + /** + * ORDER BY clause + * + * @var mixed + */ + protected $order; + + /** + * The offset, which should be applied to the select query + * + * @var int + */ + protected $offset = 0; + + /** + * The limit, which should be applied to the select query + * + * @var int + */ + protected $limit; + + /** + * Boolean to enable query debugging + * + * @var bool + */ + protected $debug = false; + + /** + * Constructor + * + * @param \Kirby\Database\Database $database Database object + * @param string $table Optional name of the table, which should be queried + */ + public function __construct(Database $database, string $table) + { + $this->database = $database; + $this->table($table); + } + + /** + * Reset the query class after each db hit + */ + protected function reset() + { + $this->bindings = []; + $this->join = null; + $this->select = null; + $this->distinct = null; + $this->fail = false; + $this->values = null; + $this->where = null; + $this->group = null; + $this->having = null; + $this->order = null; + $this->offset = 0; + $this->limit = null; + $this->debug = false; + } + + /** + * Enables query debugging. + * If enabled, the query will return an array with all important info about + * the query instead of actually executing the query and returning results + * + * @param bool $debug + * @return \Kirby\Database\Query + */ + public function debug(bool $debug = true) + { + $this->debug = $debug; + return $this; + } + + /** + * Enables distinct select clauses. + * + * @param bool $distinct + * @return \Kirby\Database\Query + */ + public function distinct(bool $distinct = true) + { + $this->distinct = $distinct; + return $this; + } + + /** + * Enables failing queries. + * If enabled queries will no longer fail silently but throw an exception + * + * @param bool $fail + * @return \Kirby\Database\Query + */ + public function fail(bool $fail = true) + { + $this->fail = $fail; + return $this; + } + + /** + * Sets the object class, which should be fetched; + * set this to `'array'` to get a simple array instead of an object; + * pass a function that receives the `$data` and the `$key` to generate arbitrary data structures + * + * @param string|\Closure $fetch + * @return \Kirby\Database\Query + */ + public function fetch($fetch) + { + $this->fetch = $fetch; + return $this; + } + + /** + * Sets the iterator class, which should be used for multiple results + * Set this to array to get a simple array instead of an iterator object + * + * @param string $iterator + * @return \Kirby\Database\Query + */ + public function iterator(string $iterator) + { + $this->iterator = $iterator; + return $this; + } + + /** + * Sets the name of the table, which should be queried + * + * @param string $table + * @return \Kirby\Database\Query + * @throws \Kirby\Exception\InvalidArgumentException if the table does not exist + */ + public function table(string $table) + { + if ($this->database->validateTable($table) === false) { + throw new InvalidArgumentException('Invalid table: ' . $table); + } + + $this->table = $table; + return $this; + } + + /** + * Sets the name of the primary key column + * + * @param string $primaryKeyName + * @return \Kirby\Database\Query + */ + public function primaryKeyName(string $primaryKeyName) + { + $this->primaryKeyName = $primaryKeyName; + return $this; + } + + /** + * Sets the columns, which should be selected from the table + * By default all columns will be selected + * + * @param mixed $select Pass either a string of columns or an array + * @return \Kirby\Database\Query + */ + public function select($select) + { + $this->select = $select; + return $this; + } + + /** + * Adds a new join clause to the query + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @param string $type The join type. Uses an inner join by default + * @return $this + */ + public function join(string $table, string $on, string $type = 'JOIN') + { + $join = [ + 'table' => $table, + 'on' => $on, + 'type' => $type + ]; + + $this->join[] = $join; + return $this; + } + + /** + * Shortcut for creating a left join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return \Kirby\Database\Query + */ + public function leftJoin(string $table, string $on) + { + return $this->join($table, $on, 'left'); + } + + /** + * Shortcut for creating a right join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return \Kirby\Database\Query + */ + public function rightJoin(string $table, string $on) + { + return $this->join($table, $on, 'right'); + } + + /** + * Shortcut for creating an inner join clause + * + * @param string $table Name of the table, which should be joined + * @param string $on The on clause for this join + * @return \Kirby\Database\Query + */ + public function innerJoin($table, $on) + { + return $this->join($table, $on, 'inner join'); + } + + /** + * Sets the values which should be used for the update or insert clause + * + * @param mixed $values Can either be a string or an array of values + * @return \Kirby\Database\Query + */ + public function values($values = []) + { + if ($values !== null) { + $this->values = $values; + } + return $this; + } + + /** + * Attaches additional bindings to the query. + * Also can be used as getter for all attached bindings by not passing an argument. + * + * @param mixed $bindings Array of bindings or null to use this method as getter + * @return array|\Kirby\Database\Query + */ + public function bindings(array $bindings = null) + { + if (is_array($bindings) === true) { + $this->bindings = array_merge($this->bindings, $bindings); + return $this; + } + + return $this->bindings; + } + + /** + * Attaches an additional where clause + * + * All available ways to add where clauses + * + * ->where('username like "myuser"'); (args: 1) + * ->where(['username' => 'myuser']); (args: 1) + * ->where(function($where) { $where->where('id', '=', 1) }) (args: 1) + * ->where('username like ?', 'myuser') (args: 2) + * ->where('username', 'like', 'myuser'); (args: 3) + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function where(...$args) + { + $this->where = $this->filterQuery($args, $this->where); + return $this; + } + + /** + * Shortcut to attach a where clause with an OR operator. + * Check out the where() method docs for additional info. + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function orWhere(...$args) + { + $mode = A::last($args); + + // if there's a where clause mode attribute attached… + if (in_array($mode, ['AND', 'OR'], true) === true) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the OR mode indicator + $args[] = 'OR'; + + $this->where(...$args); + return $this; + } + + /** + * Shortcut to attach a where clause with an AND operator. + * Check out the where() method docs for additional info. + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function andWhere(...$args) + { + $mode = A::last($args); + + // if there's a where clause mode attribute attached… + if (in_array($mode, ['AND', 'OR'], true) === true) { + // remove that from the list of arguments + array_pop($args); + } + + // make sure to always attach the AND mode indicator + $args[] = 'AND'; + + $this->where(...$args); + return $this; + } + + /** + * Attaches a group by clause + * + * @param string|null $group + * @return \Kirby\Database\Query + */ + public function group(string $group = null) + { + $this->group = $group; + return $this; + } + + /** + * Attaches an additional having clause + * + * All available ways to add having clauses + * + * ->having('username like "myuser"'); (args: 1) + * ->having(['username' => 'myuser']); (args: 1) + * ->having(function($having) { $having->having('id', '=', 1) }) (args: 1) + * ->having('username like ?', 'myuser') (args: 2) + * ->having('username', 'like', 'myuser'); (args: 3) + * + * @param mixed ...$args + * @return \Kirby\Database\Query + */ + public function having(...$args) + { + $this->having = $this->filterQuery($args, $this->having); + return $this; + } + + /** + * Attaches an order clause + * + * @param string|null $order + * @return \Kirby\Database\Query + */ + public function order(string $order = null) + { + $this->order = $order; + return $this; + } + + /** + * Sets the offset for select clauses + * + * @param int|null $offset + * @return \Kirby\Database\Query + */ + public function offset(int $offset = null) + { + $this->offset = $offset; + return $this; + } + + /** + * Sets the limit for select clauses + * + * @param int|null $limit + * @return \Kirby\Database\Query + */ + public function limit(int $limit = null) + { + $this->limit = $limit; + return $this; + } + + /** + * Builds the different types of SQL queries + * This uses the SQL class to build stuff. + * + * @param string $type (select, update, insert) + * @return array The final query + */ + public function build(string $type) + { + $sql = $this->database->sql(); + + switch ($type) { + case 'select': + return $sql->select([ + 'table' => $this->table, + 'columns' => $this->select, + 'join' => $this->join, + 'distinct' => $this->distinct, + 'where' => $this->where, + 'group' => $this->group, + 'having' => $this->having, + 'order' => $this->order, + 'offset' => $this->offset, + 'limit' => $this->limit, + 'bindings' => $this->bindings + ]); + case 'update': + return $sql->update([ + 'table' => $this->table, + 'where' => $this->where, + 'values' => $this->values, + 'bindings' => $this->bindings + ]); + case 'insert': + return $sql->insert([ + 'table' => $this->table, + 'values' => $this->values, + 'bindings' => $this->bindings + ]); + case 'delete': + return $sql->delete([ + 'table' => $this->table, + 'where' => $this->where, + 'bindings' => $this->bindings + ]); + } + } + + /** + * Builds a count query + * + * @return int + */ + public function count(): int + { + return (int)$this->aggregate('COUNT'); + } + + /** + * Builds a max query + * + * @param string $column + * @return float + */ + public function max(string $column): float + { + return (float)$this->aggregate('MAX', $column); + } + + /** + * Builds a min query + * + * @param string $column + * @return float + */ + public function min(string $column): float + { + return (float)$this->aggregate('MIN', $column); + } + + /** + * Builds a sum query + * + * @param string $column + * @return float + */ + public function sum(string $column): float + { + return (float)$this->aggregate('SUM', $column); + } + + /** + * Builds an average query + * + * @param string $column + * @return float + */ + public function avg(string $column): float + { + return (float)$this->aggregate('AVG', $column); + } + + /** + * Builds an aggregation query. + * This is used by all the aggregation methods above + * + * @param string $method + * @param string $column + * @param int $default An optional default value, which should be returned if the query fails + * @return mixed + */ + public function aggregate(string $method, string $column = '*', $default = 0) + { + // reset the sorting to avoid counting issues + $this->order = null; + + // validate column + if ($column !== '*') { + $sql = $this->database->sql(); + $column = $sql->columnName($this->table, $column); + } + + $fetch = $this->fetch; + $row = $this->select($method . '(' . $column . ') as aggregation')->fetch('Obj')->first(); + + if ($this->debug === true) { + return $row; + } + + $result = $row ? $row->get('aggregation') : $default; + + $this->fetch($fetch); + + return $result; + } + + /** + * Used as an internal shortcut for firing a db query + * + * @param string|array $sql + * @param array $params + * @return mixed + */ + protected function query($sql, array $params = []) + { + if (is_string($sql) === true) { + $sql = [ + 'query' => $sql, + 'bindings' => $this->bindings() + ]; + } + + if ($this->debug) { + return [ + 'query' => $sql['query'], + 'bindings' => $this->bindings(), + 'options' => $params + ]; + } + + if ($this->fail) { + $this->database->fail(); + } + + $result = $this->database->query($sql['query'], $sql['bindings'], $params); + + $this->reset(); + + return $result; + } + + /** + * Used as an internal shortcut for executing a db query + * + * @param string|array $sql + * @param array $params + * @return mixed + */ + protected function execute($sql, array $params = []) + { + if (is_string($sql) === true) { + $sql = [ + 'query' => $sql, + 'bindings' => $this->bindings() + ]; + } + + if ($this->debug === true) { + return [ + 'query' => $sql['query'], + 'bindings' => $sql['bindings'], + 'options' => $params + ]; + } + + if ($this->fail) { + $this->database->fail(); + } + + $result = $this->database->execute($sql['query'], $sql['bindings']); + + $this->reset(); + + return $result; + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function first() + { + return $this->query($this->offset(0)->limit(1)->build('select'), [ + 'fetch' => $this->fetch, + 'iterator' => 'array', + 'method' => 'fetch', + ]); + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function row() + { + return $this->first(); + } + + /** + * Selects only one row from a table + * + * @return object + */ + public function one() + { + return $this->first(); + } + + /** + * Automatically adds pagination to a query + * + * @param int $page + * @param int $limit The number of rows, which should be returned for each page + * @return object Collection iterator with attached pagination object + */ + public function page(int $page, int $limit) + { + // clone this to create a counter query + $counter = clone $this; + + // count the total number of rows for this query + $count = $counter->debug(false)->count(); + + // pagination + $pagination = new Pagination([ + 'limit' => $limit, + 'page' => $page, + 'total' => $count, + ]); + + // apply it to the dataset and retrieve all rows. make sure to use Collection as the iterator to be able to attach the pagination object + $iterator = $this->iterator; + $collection = $this + ->offset($pagination->offset()) + ->limit($pagination->limit()) + ->iterator('Kirby\Toolkit\Collection') + ->all(); + + $this->iterator($iterator); + + // return debug information if debug mode is active + if ($this->debug) { + $collection['totalcount'] = $count; + return $collection; + } + + // store all pagination vars in a separate object + if ($collection) { + $collection->paginate($pagination); + } + + // return the limited collection + return $collection; + } + + /** + * Returns all matching rows from a table + * + * @return mixed + */ + public function all() + { + return $this->query($this->build('select'), [ + 'fetch' => $this->fetch, + 'iterator' => $this->iterator, + ]); + } + + /** + * Returns only values from a single column + * + * @param string $column + * @return mixed + */ + public function column(string $column) + { + // if there isn't already an explicit order, order by the primary key + // instead of the column that was requested (which would be implied otherwise) + if ($this->order === null) { + $sql = $this->database->sql(); + $primaryKey = $sql->combineIdentifier($this->table, $this->primaryKeyName); + + $this->order($primaryKey . ' ASC'); + } + + $results = $this->query($this->select([$column])->build('select'), [ + 'iterator' => 'array', + 'fetch' => 'array', + ]); + + if ($this->debug === true) { + return $results; + } + + $results = array_column($results, $column); + + if ($this->iterator === 'array') { + return $results; + } + + $iterator = $this->iterator; + + return new $iterator($results); + } + + /** + * Find a single row by column and value + * + * @param string $column + * @param mixed $value + * @return mixed + */ + public function findBy(string $column, $value) + { + return $this->where([$column => $value])->first(); + } + + /** + * Find a single row by its primary key + * + * @param mixed $id + * @return mixed + */ + public function find($id) + { + return $this->findBy($this->primaryKeyName, $id); + } + + /** + * Fires an insert query + * + * @param mixed $values You can pass values here or set them with ->values() before + * @return mixed Returns the last inserted id on success or false. + */ + public function insert($values = null) + { + $query = $this->execute($this->values($values)->build('insert')); + + if ($this->debug === true) { + return $query; + } + + return $query ? $this->database->lastId() : false; + } + + /** + * Fires an update query + * + * @param mixed $values You can pass values here or set them with ->values() before + * @param mixed $where You can pass a where clause here or set it with ->where() before + * @return bool + */ + public function update($values = null, $where = null) + { + return $this->execute($this->values($values)->where($where)->build('update')); + } + + /** + * Fires a delete query + * + * @param mixed $where You can pass a where clause here or set it with ->where() before + * @return bool + */ + public function delete($where = null) + { + return $this->execute($this->where($where)->build('delete')); + } + + /** + * Enables magic queries like findByUsername or findByEmail + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + if (preg_match('!^findBy([a-z]+)!i', $method, $match)) { + $column = Str::lower($match[1]); + return $this->findBy($column, $arguments[0]); + } else { + throw new InvalidArgumentException('Invalid query method: ' . $method, static::ERROR_INVALID_QUERY_METHOD); + } + } + + /** + * Builder for where and having clauses + * + * @param array $args Arguments, see where() description + * @param mixed $current Current value (like $this->where) + * @return string + */ + protected function filterQuery(array $args, $current) + { + $mode = A::last($args); + $result = ''; + + // if there's a where clause mode attribute attached… + if (in_array($mode, ['AND', 'OR'], true) === true) { + // remove that from the list of arguments + array_pop($args); + } else { + $mode = 'AND'; + } + + switch (count($args)) { + case 1: + + if ($args[0] === null) { + return $current; + + // ->where('username like "myuser"'); + } elseif (is_string($args[0]) === true) { + // simply add the entire string to the where clause + // escaping or using bindings has to be done before calling this method + $result = $args[0]; + + // ->where(['username' => 'myuser']); + } elseif (is_array($args[0]) === true) { + // simple array mode (AND operator) + $sql = $this->database->sql()->values($this->table, $args[0], ' AND ', true, true); + + $result = $sql['query']; + + $this->bindings($sql['bindings']); + } elseif (is_callable($args[0]) === true) { + $query = clone $this; + + // since the callback uses its own where condition + // it is necessary to clear/reset the cloned where condition + $query->where = null; + + call_user_func($args[0], $query); + + // copy over the bindings from the nested query + $this->bindings = array_merge($this->bindings, $query->bindings); + + $result = '(' . $query->where . ')'; + } + + break; + case 2: + + // ->where('username like :username', ['username' => 'myuser']) + if (is_string($args[0]) === true && is_array($args[1]) === true) { + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings($args[1]); + + // ->where('username like ?', 'myuser') + } elseif (is_string($args[0]) === true && is_string($args[1]) === true) { + // prepared where clause + $result = $args[0]; + + // store the bindings + $this->bindings([$args[1]]); + } + + break; + case 3: + + // ->where('username', 'like', 'myuser'); + if (is_string($args[0]) === true && is_string($args[1]) === true) { + // validate column + $sql = $this->database->sql(); + $key = $sql->columnName($this->table, $args[0]); + + // ->where('username', 'in', ['myuser', 'myotheruser']); + $predicate = trim(strtoupper($args[1])); + if (is_array($args[2]) === true) { + if (in_array($predicate, ['IN', 'NOT IN']) === false) { + throw new InvalidArgumentException('Invalid predicate ' . $predicate); + } + + // build a list of bound values + $values = []; + $bindings = []; + + foreach ($args[2] as $value) { + $valueBinding = $sql->bindingName('value'); + $bindings[$valueBinding] = $value; + $values[] = $valueBinding; + } + + // add that to the where clause in parenthesis + $result = $key . ' ' . $predicate . ' (' . implode(', ', $values) . ')'; + + // ->where('username', 'like', 'myuser'); + } else { + $predicates = [ + '=', '>=', '>', '<=', '<', '<>', '!=', '<=>', + 'IS', 'IS NOT', + 'BETWEEN', 'NOT BETWEEN', + 'LIKE', 'NOT LIKE', + 'SOUNDS LIKE', + 'REGEXP', 'NOT REGEXP' + ]; + + if (in_array($predicate, $predicates) === false) { + throw new InvalidArgumentException('Invalid predicate/operator ' . $predicate); + } + + $valueBinding = $sql->bindingName('value'); + $bindings[$valueBinding] = $args[2]; + + $result = $key . ' ' . $predicate . ' ' . $valueBinding; + } + $this->bindings($bindings); + } + + break; + } + + // attach the where clause + if (empty($current) === false) { + return $current . ' ' . $mode . ' ' . $result; + } else { + return $result; + } + } } diff --git a/kirby/src/Database/Sql.php b/kirby/src/Database/Sql.php index f0ab6ac..e3cff62 100644 --- a/kirby/src/Database/Sql.php +++ b/kirby/src/Database/Sql.php @@ -17,940 +17,950 @@ use Kirby\Toolkit\Str; */ abstract class Sql { - /** - * List of literals which should not be escaped in queries - * - * @var array - */ - public static $literals = ['NOW()', null]; - - /** - * The parent database connection - * - * @var \Kirby\Database\Database - */ - protected $database; - - /** - * List of used bindings; used to avoid - * duplicate binding names - * - * @var array - */ - protected $bindings = []; - - /** - * Constructor - * @codeCoverageIgnore - * - * @param \Kirby\Database\Database $database - */ - public function __construct($database) - { - $this->database = $database; - } - - /** - * Returns a randomly generated binding name - * - * @param string $label String that only contains alphanumeric chars and - * underscores to use as a human-readable identifier - * @return string Binding name that is guaranteed to be unique for this connection - */ - public function bindingName(string $label): string - { - // make sure that the binding name is safe to prevent injections; - // otherwise use a generic label - if (!$label || preg_match('/^[a-zA-Z0-9_]+$/', $label) !== 1) { - $label = 'invalid'; - } - - // generate random bindings until the name is unique - do { - $binding = ':' . $label . '_' . Str::random(8, 'alphaNum'); - } while (in_array($binding, $this->bindings) === true); - - // cache the generated binding name for future invocations - $this->bindings[] = $binding; - return $binding; - } - - /** - * Returns a query to list the columns of a specified table; - * the query needs to return rows with a column `name` - * - * @param string $table Table name - * @return array - */ - abstract public function columns(string $table): array; - - /** - * Returns a query snippet for a column default value - * - * @param string $name Column name - * @param array $column Column definition array with an optional `default` key - * @return array Array with a `query` string and a `bindings` array - */ - public function columnDefault(string $name, array $column): array - { - if (isset($column['default']) === false) { - return [ - 'query' => null, - 'bindings' => [] - ]; - } - - $binding = $this->bindingName($name . '_default'); - - return [ - 'query' => 'DEFAULT ' . $binding, - 'bindings' => [ - $binding => $column['default'] - ] - ]; - } - - /** - * Returns the cleaned identifier based on the table and column name - * - * @param string $table Table name - * @param string $column Column name - * @param bool $enforceQualified If true, a qualified identifier is returned in all cases - * @return string|null Identifier or null if the table or column is invalid - */ - public function columnName(string $table, string $column, bool $enforceQualified = false): ?string - { - // ensure we have clean $table and $column values without qualified identifiers - list($table, $column) = $this->splitIdentifier($table, $column); - - // combine the identifiers again - if ($this->database->validateColumn($table, $column) === true) { - return $this->combineIdentifier($table, $column, $enforceQualified !== true); - } - - // the table or column does not exist - return null; - } - - /** - * Abstracted column types to simplify table - * creation for multiple database drivers - * @codeCoverageIgnore - * - * @return array - */ - public function columnTypes(): array - { - return [ - 'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY', - 'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }} {{ unique }}', - 'text' => '{{ name }} TEXT {{ unique }}', - 'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }} {{ unique }}', - 'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }} {{ unique }}' - ]; - } - - /** - * Combines an identifier (table and column) - * - * @param $table string - * @param $column string - * @param $values bool Whether the identifier is going to be used for a VALUES clause; - * only relevant for SQLite - * @return string - */ - public function combineIdentifier(string $table, string $column, bool $values = false): string - { - return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); - } - - /** - * Creates the CREATE TABLE syntax for a single column - * - * @param string $name Column name - * @param array $column Column definition array; valid keys: - * - `type` (required): Column template to use - * - `null`: Whether the column may be NULL (boolean) - * - `key`: Index this column is part of; special values `'primary'` for PRIMARY KEY and `true` for automatic naming - * - `unique`: Whether the index (or if not set the column itself) has a UNIQUE constraint - * - `default`: Default value of this column - * @return array Array with `query` and `key` strings, a `unique` boolean and a `bindings` array - * @throws \Kirby\Exception\InvalidArgumentException if no column type is given or the column type is not supported. - */ - public function createColumn(string $name, array $column): array - { - // column type - if (isset($column['type']) === false) { - throw new InvalidArgumentException('No column type given for column ' . $name); - } - $template = $this->columnTypes()[$column['type']] ?? null; - if (!$template) { - throw new InvalidArgumentException('Unsupported column type: ' . $column['type']); - } - - // null option - if (A::get($column, 'null') === false) { - $null = 'NOT NULL'; - } else { - $null = 'NULL'; - } - - // indexes/keys - if (isset($column['key']) === true) { - if (is_string($column['key']) === true) { - $column['key'] = strtolower($column['key']); - } elseif ($column['key'] === true) { - $column['key'] = $name . '_index'; - } - } - - // unique - $uniqueKey = false; - $uniqueColumn = null; - if (isset($column['unique']) === true && $column['unique'] === true) { - if (isset($column['key']) === true) { - // this column is part of an index, make that unique - $uniqueKey = true; - } else { - // make the column itself unique - $uniqueColumn = 'UNIQUE'; - } - } - - // default value - $columnDefault = $this->columnDefault($name, $column); - - $query = trim(Str::template($template, [ - 'name' => $this->quoteIdentifier($name), - 'null' => $null, - 'default' => $columnDefault['query'], - 'unique' => $uniqueColumn - ], ['fallback' => ''])); - - return [ - 'query' => $query, - 'bindings' => $columnDefault['bindings'], - 'key' => $column['key'] ?? null, - 'unique' => $uniqueKey - ]; - } - - /** - * Creates the inner query for the columns in a CREATE TABLE query - * - * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` - * @return array Array with a `query` string and `bindings`, `keys` and `unique` arrays - */ - public function createTableInner(array $columns): array - { - $query = []; - $bindings = []; - $keys = []; - $unique = []; - - foreach ($columns as $name => $column) { - $sql = $this->createColumn($name, $column); - - // collect query and bindings - $query[] = $sql['query']; - $bindings += $sql['bindings']; - - // make a list of keys per key name - if ($sql['key'] !== null) { - if (isset($keys[$sql['key']]) !== true) { - $keys[$sql['key']] = []; - } - - $keys[$sql['key']][] = $name; - if ($sql['unique'] === true) { - $unique[$sql['key']] = true; - } - } - } - - return [ - 'query' => implode(',' . PHP_EOL, $query), - 'bindings' => $bindings, - 'keys' => $keys, - 'unique' => $unique - ]; - } - - /** - * Creates a CREATE TABLE query - * - * @param string $table Table name - * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` - * @return array Array with a `query` string and a `bindings` array - */ - public function createTable(string $table, array $columns = []): array - { - $inner = $this->createTableInner($columns); - - // add keys - foreach ($inner['keys'] as $key => $columns) { - // quote each column name and make a list string out of the column names - $columns = implode(', ', array_map( - fn ($name) => $this->quoteIdentifier($name), - $columns - )); - - if ($key === 'primary') { - $key = 'PRIMARY KEY'; - } else { - $unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : ''; - $key = $unique . 'INDEX ' . $this->quoteIdentifier($key); - } - - $inner['query'] .= ',' . PHP_EOL . $key . ' (' . $columns . ')'; - } - - return [ - 'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')', - 'bindings' => $inner['bindings'] - ]; - } - - /** - * Builds a DELETE clause - * - * @param array $params List of parameters for the DELETE clause. See defaults for more info. - * @return array - */ - public function delete(array $params = []): array - { - $defaults = [ - 'table' => '', - 'where' => null, - 'bindings' => [] - ]; - - $options = array_merge($defaults, $params); - $bindings = $options['bindings']; - $query = ['DELETE']; - - // from - $this->extend($query, $bindings, $this->from($options['table'])); - - // where - $this->extend($query, $bindings, $this->where($options['where'])); - - return [ - 'query' => $this->query($query), - 'bindings' => $bindings - ]; - } - - /** - * Creates the sql for dropping a single table - * - * @param string $table - * @return array - */ - public function dropTable(string $table): array - { - return [ - 'query' => 'DROP TABLE ' . $this->tableName($table), - 'bindings' => [] - ]; - } - - /** - * Extends a given query and bindings - * by reference - * - * @param array $query - * @param array $bindings - * @param array $input - * @return void - */ - public function extend(&$query, array &$bindings, $input) - { - if (empty($input['query']) === false) { - $query[] = $input['query']; - $bindings = array_merge($bindings, $input['bindings']); - } - } - - /** - * Creates the from syntax - * - * @param string $table - * @return array - */ - public function from(string $table): array - { - return [ - 'query' => 'FROM ' . $this->tableName($table), - 'bindings' => [] - ]; - } - - /** - * Creates the group by syntax - * - * @param string $group - * @return array - */ - public function group(string $group = null): array - { - if (empty($group) === true) { - return [ - 'query' => null, - 'bindings' => [] - ]; - } - - return [ - 'query' => 'GROUP BY ' . $group, - 'bindings' => [] - ]; - } - - /** - * Creates the having syntax - * - * @param string|null $having - * @return array - */ - public function having(string $having = null): array - { - if (empty($having) === true) { - return [ - 'query' => null, - 'bindings' => [] - ]; - } - - return [ - 'query' => 'HAVING ' . $having, - 'bindings' => [] - ]; - } - - /** - * Creates an insert query - * - * @param array $params - * @return array - */ - public function insert(array $params = []): array - { - $table = $params['table'] ?? null; - $values = $params['values'] ?? null; - $bindings = $params['bindings']; - $query = ['INSERT INTO ' . $this->tableName($table)]; - - // add the values - $this->extend($query, $bindings, $this->values($table, $values, ', ', false)); - - return [ - 'query' => $this->query($query), - 'bindings' => $bindings - ]; - } - - /** - * Creates a join query - * - * @param string $table - * @param string $type - * @param string $on - * @return array - * @throws \Kirby\Exception\InvalidArgumentException if an invalid join type is given - */ - public function join(string $type, string $table, string $on): array - { - $types = [ - 'JOIN', - 'INNER JOIN', - 'OUTER JOIN', - 'LEFT OUTER JOIN', - 'LEFT JOIN', - 'RIGHT OUTER JOIN', - 'RIGHT JOIN', - 'FULL OUTER JOIN', - 'FULL JOIN', - 'NATURAL JOIN', - 'CROSS JOIN', - 'SELF JOIN' - ]; - - $type = strtoupper(trim($type)); - - // validate join type - if (in_array($type, $types) === false) { - throw new InvalidArgumentException('Invalid join type ' . $type); - } - - return [ - 'query' => $type . ' ' . $this->tableName($table) . ' ON ' . $on, - 'bindings' => [], - ]; - } - - /** - * Create the syntax for multiple joins - * - * @param array|null $joins - * @return array - */ - public function joins(array $joins = null): array - { - $query = []; - $bindings = []; - - foreach ((array)$joins as $join) { - $this->extend($query, $bindings, $this->join($join['type'] ?? 'JOIN', $join['table'] ?? null, $join['on'] ?? null)); - } - - return [ - 'query' => implode(' ', array_filter($query)), - 'bindings' => [], - ]; - } - - /** - * Creates a limit and offset query instruction - * - * @param int $offset - * @param int|null $limit - * @return array - */ - public function limit(int $offset = 0, int $limit = null): array - { - // no need to add it to the query - if ($offset === 0 && $limit === null) { - return [ - 'query' => null, - 'bindings' => [] - ]; - } - - $limit ??= '18446744073709551615'; - - $offsetBinding = $this->bindingName('offset'); - $limitBinding = $this->bindingName('limit'); - - return [ - 'query' => 'LIMIT ' . $offsetBinding . ', ' . $limitBinding, - 'bindings' => [ - $limitBinding => $limit, - $offsetBinding => $offset, - ] - ]; - } - - /** - * Creates the order by syntax - * - * @param string $order - * @return array - */ - public function order(string $order = null): array - { - if (empty($order) === true) { - return [ - 'query' => null, - 'bindings' => [] - ]; - } - - return [ - 'query' => 'ORDER BY ' . $order, - 'bindings' => [] - ]; - } - - /** - * Converts a query array into a final string - * - * @param array $query - * @param string $separator - * @return string - */ - public function query(array $query, string $separator = ' ') - { - return implode($separator, array_filter($query)); - } - - /** - * Quotes an identifier (table *or* column) - * - * @param $identifier string - * @return string - */ - public function quoteIdentifier(string $identifier): string - { - // * is special, don't quote that - if ($identifier === '*') { - return $identifier; - } - - // escape backticks inside the identifier name - $identifier = str_replace('`', '``', $identifier); - - // wrap in backticks - return '`' . $identifier . '`'; - } - - /** - * Builds a select clause - * - * @param array $params List of parameters for the select clause. Check out the defaults for more info. - * @return array An array with the query and the bindings - */ - public function select(array $params = []): array - { - $defaults = [ - 'table' => '', - 'columns' => '*', - 'join' => null, - 'distinct' => false, - 'where' => null, - 'group' => null, - 'having' => null, - 'order' => null, - 'offset' => 0, - 'limit' => null, - 'bindings' => [] - ]; - - $options = array_merge($defaults, $params); - $bindings = $options['bindings']; - $query = ['SELECT']; - - // select distinct values - if ($options['distinct'] === true) { - $query[] = 'DISTINCT'; - } - - // columns - $query[] = $this->selected($options['table'], $options['columns']); - - // from - $this->extend($query, $bindings, $this->from($options['table'])); - - // joins - $this->extend($query, $bindings, $this->joins($options['join'])); - - // where - $this->extend($query, $bindings, $this->where($options['where'])); - - // group - $this->extend($query, $bindings, $this->group($options['group'])); - - // having - $this->extend($query, $bindings, $this->having($options['having'])); - - // order - $this->extend($query, $bindings, $this->order($options['order'])); - - // offset and limit - $this->extend($query, $bindings, $this->limit($options['offset'], $options['limit'])); - - return [ - 'query' => $this->query($query), - 'bindings' => $bindings - ]; - } - - /** - * Creates a columns definition from string or array - * - * @param string $table - * @param array|string|null $columns - * @return string - */ - public function selected($table, $columns = null): string - { - // all columns - if (empty($columns) === true) { - return '*'; - } - - // array of columns - if (is_array($columns) === true) { - - // validate columns - $result = []; - - foreach ($columns as $column) { - list($table, $columnPart) = $this->splitIdentifier($table, $column); - - if ($this->validateColumn($table, $columnPart) === true) { - $result[] = $this->combineIdentifier($table, $columnPart); - } - } - - return implode(', ', $result); - } else { - return $columns; - } - } - - /** - * Splits a (qualified) identifier into table and column - * - * @param $table string Default table if the identifier is not qualified - * @param $identifier string - * @return array - * @throws \Kirby\Exception\InvalidArgumentException if an invalid identifier is given - */ - public function splitIdentifier($table, $identifier): array - { - // split by dot, but only outside of quotes - $parts = preg_split('/(?:`[^`]*`|"[^"]*")(*SKIP)(*F)|\./', $identifier); - - switch (count($parts)) { - // non-qualified identifier - case 1: - return [$table, $this->unquoteIdentifier($parts[0])]; - - // qualified identifier - case 2: - return [$this->unquoteIdentifier($parts[0]), $this->unquoteIdentifier($parts[1])]; - - // every other number is an error - default: - throw new InvalidArgumentException('Invalid identifier ' . $identifier); - } - } - - /** - * Returns a query to list the tables of the current database; - * the query needs to return rows with a column `name` - * - * @return array - */ - abstract public function tables(): array; - - /** - * Validates and quotes a table name - * - * @param string $table - * @return string - * @throws \Kirby\Exception\InvalidArgumentException if an invalid table name is given - */ - public function tableName(string $table): string - { - // validate table - if ($this->database->validateTable($table) === false) { - throw new InvalidArgumentException('Invalid table ' . $table); - } - - return $this->quoteIdentifier($table); - } - - /** - * Unquotes an identifier (table *or* column) - * - * @param $identifier string - * @return string - */ - public function unquoteIdentifier(string $identifier): string - { - // remove quotes around the identifier - if (in_array(Str::substr($identifier, 0, 1), ['"', '`']) === true) { - $identifier = Str::substr($identifier, 1); - } - - if (in_array(Str::substr($identifier, -1), ['"', '`']) === true) { - $identifier = Str::substr($identifier, 0, -1); - } - - // unescape duplicated quotes - return str_replace(['""', '``'], ['"', '`'], $identifier); - } - - /** - * Builds an update clause - * - * @param array $params List of parameters for the update clause. See defaults for more info. - * @return array - */ - public function update(array $params = []): array - { - $defaults = [ - 'table' => null, - 'values' => null, - 'where' => null, - 'bindings' => [] - ]; - - $options = array_merge($defaults, $params); - $bindings = $options['bindings']; - - // start the query - $query = ['UPDATE ' . $this->tableName($options['table']) . ' SET']; - - // add the values - $this->extend($query, $bindings, $this->values($options['table'], $options['values'])); - - // add the where clause - $this->extend($query, $bindings, $this->where($options['where'])); - - return [ - 'query' => $this->query($query), - 'bindings' => $bindings - ]; - } - - /** - * Validates a given column name in a table - * - * @param string $table - * @param string $column - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException If the column is invalid - */ - public function validateColumn(string $table, string $column): bool - { - if ($this->database->validateColumn($table, $column) !== true) { - throw new InvalidArgumentException('Invalid column ' . $column); - } - - return true; - } - - /** - * Builds a safe list of values for insert, select or update queries - * - * @param string $table Table name - * @param mixed $values A value string or array of values - * @param string $separator A separator which should be used to join values - * @param bool $set If true builds a set list of values for update clauses - * @param bool $enforceQualified Always use fully qualified column names - */ - public function values(string $table, $values, string $separator = ', ', bool $set = true, bool $enforceQualified = false): array - { - if (is_array($values) === false) { - return [ - 'query' => $values, - 'bindings' => [] - ]; - } - - if ($set === true) { - return $this->valueSet($table, $values, $separator, $enforceQualified); - } else { - return $this->valueList($table, $values, $separator, $enforceQualified); - } - } - - /** - * Creates a list of fields and values - * - * @param string $table - * @param string|array $values - * @param string $separator - * @param bool $enforceQualified - * @param array - */ - public function valueList(string $table, $values, string $separator = ',', bool $enforceQualified = false): array - { - $fields = []; - $query = []; - $bindings = []; - - foreach ($values as $key => $value) { - $fields[] = $this->columnName($table, $key, $enforceQualified); - - if (in_array($value, static::$literals, true) === true) { - $query[] = $value ?: 'null'; - continue; - } - - if (is_array($value) === true) { - $value = json_encode($value); - } - - // add the binding - $bindings[$bindingName = $this->bindingName('value')] = $value; - - // create the query - $query[] = $bindingName; - } - - return [ - 'query' => '(' . implode($separator, $fields) . ') VALUES (' . implode($separator, $query) . ')', - 'bindings' => $bindings - ]; - } - - /** - * Creates a set of values - * - * @param string $table - * @param string|array $values - * @param string $separator - * @param bool $enforceQualified - * @param array - * @return array - */ - public function valueSet(string $table, $values, string $separator = ',', bool $enforceQualified = false): array - { - $query = []; - $bindings = []; - - foreach ($values as $column => $value) { - $key = $this->columnName($table, $column, $enforceQualified); - - if (in_array($value, static::$literals, true) === true) { - $query[] = $key . ' = ' . ($value ?: 'null'); - continue; - } - - if (is_array($value) === true) { - $value = json_encode($value); - } - - // add the binding - $bindings[$bindingName = $this->bindingName('value')] = $value; - - // create the query - $query[] = $key . ' = ' . $bindingName; - } - - return [ - 'query' => implode($separator, $query), - 'bindings' => $bindings - ]; - } - - /** - * @param string|array|null $where - * @param array $bindings - * @return array - */ - public function where($where, array $bindings = []): array - { - if (empty($where) === true) { - return [ - 'query' => null, - 'bindings' => [], - ]; - } - - if (is_string($where) === true) { - return [ - 'query' => 'WHERE ' . $where, - 'bindings' => $bindings - ]; - } - - $query = []; - - foreach ($where as $key => $value) { - $binding = $this->bindingName('where_' . $key); - $bindings[$binding] = $value; - - $query[] = $key . ' = ' . $binding; - } - - return [ - 'query' => 'WHERE ' . implode(' AND ', $query), - 'bindings' => $bindings - ]; - } + /** + * List of literals which should not be escaped in queries + * + * @var array + */ + public static $literals = ['NOW()', null]; + + /** + * The parent database connection + * + * @var \Kirby\Database\Database + */ + protected $database; + + /** + * List of used bindings; used to avoid + * duplicate binding names + * + * @var array + */ + protected $bindings = []; + + /** + * Constructor + * @codeCoverageIgnore + * + * @param \Kirby\Database\Database $database + */ + public function __construct($database) + { + $this->database = $database; + } + + /** + * Returns a randomly generated binding name + * + * @param string $label String that only contains alphanumeric chars and + * underscores to use as a human-readable identifier + * @return string Binding name that is guaranteed to be unique for this connection + */ + public function bindingName(string $label): string + { + // make sure that the binding name is safe to prevent injections; + // otherwise use a generic label + if (!$label || preg_match('/^[a-zA-Z0-9_]+$/', $label) !== 1) { + $label = 'invalid'; + } + + // generate random bindings until the name is unique + do { + $binding = ':' . $label . '_' . Str::random(8, 'alphaNum'); + } while (in_array($binding, $this->bindings) === true); + + // cache the generated binding name for future invocations + $this->bindings[] = $binding; + return $binding; + } + + /** + * Returns a query to list the columns of a specified table; + * the query needs to return rows with a column `name` + * + * @param string $table Table name + * @return array + */ + abstract public function columns(string $table): array; + + /** + * Returns a query snippet for a column default value + * + * @param string $name Column name + * @param array $column Column definition array with an optional `default` key + * @return array Array with a `query` string and a `bindings` array + */ + public function columnDefault(string $name, array $column): array + { + if (isset($column['default']) === false) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + $binding = $this->bindingName($name . '_default'); + + return [ + 'query' => 'DEFAULT ' . $binding, + 'bindings' => [ + $binding => $column['default'] + ] + ]; + } + + /** + * Returns the cleaned identifier based on the table and column name + * + * @param string $table Table name + * @param string $column Column name + * @param bool $enforceQualified If true, a qualified identifier is returned in all cases + * @return string|null Identifier or null if the table or column is invalid + */ + public function columnName(string $table, string $column, bool $enforceQualified = false): ?string + { + // ensure we have clean $table and $column values without qualified identifiers + list($table, $column) = $this->splitIdentifier($table, $column); + + // combine the identifiers again + if ($this->database->validateColumn($table, $column) === true) { + return $this->combineIdentifier($table, $column, $enforceQualified !== true); + } + + // the table or column does not exist + return null; + } + + /** + * Abstracted column types to simplify table + * creation for multiple database drivers + * @codeCoverageIgnore + * + * @return array + */ + public function columnTypes(): array + { + return [ + 'id' => '{{ name }} INT(11) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY', + 'varchar' => '{{ name }} varchar(255) {{ null }} {{ default }} {{ unique }}', + 'text' => '{{ name }} TEXT {{ unique }}', + 'int' => '{{ name }} INT(11) UNSIGNED {{ null }} {{ default }} {{ unique }}', + 'timestamp' => '{{ name }} TIMESTAMP {{ null }} {{ default }} {{ unique }}', + 'bool' => '{{ name }} TINYINT(1) {{ null }} {{ default }} {{ unique }}' + ]; + } + + /** + * Combines an identifier (table and column) + * + * @param $table string + * @param $column string + * @param $values bool Whether the identifier is going to be used for a VALUES clause; + * only relevant for SQLite + * @return string + */ + public function combineIdentifier(string $table, string $column, bool $values = false): string + { + return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); + } + + /** + * Creates the CREATE TABLE syntax for a single column + * + * @param string $name Column name + * @param array $column Column definition array; valid keys: + * - `type` (required): Column template to use + * - `null`: Whether the column may be NULL (boolean) + * - `key`: Index this column is part of; special values `'primary'` for PRIMARY KEY and `true` for automatic naming + * - `unique`: Whether the index (or if not set the column itself) has a UNIQUE constraint + * - `default`: Default value of this column + * @return array Array with `query` and `key` strings, a `unique` boolean and a `bindings` array + * @throws \Kirby\Exception\InvalidArgumentException if no column type is given or the column type is not supported. + */ + public function createColumn(string $name, array $column): array + { + // column type + if (isset($column['type']) === false) { + throw new InvalidArgumentException('No column type given for column ' . $name); + } + $template = $this->columnTypes()[$column['type']] ?? null; + if (!$template) { + throw new InvalidArgumentException('Unsupported column type: ' . $column['type']); + } + + // null option + if (A::get($column, 'null') === false) { + $null = 'NOT NULL'; + } else { + $null = 'NULL'; + } + + // indexes/keys + if (isset($column['key']) === true) { + if (is_string($column['key']) === true) { + $column['key'] = strtolower($column['key']); + } elseif ($column['key'] === true) { + $column['key'] = $name . '_index'; + } + } + + // unique + $uniqueKey = false; + $uniqueColumn = null; + if (isset($column['unique']) === true && $column['unique'] === true) { + if (isset($column['key']) === true) { + // this column is part of an index, make that unique + $uniqueKey = true; + } else { + // make the column itself unique + $uniqueColumn = 'UNIQUE'; + } + } + + // default value + $columnDefault = $this->columnDefault($name, $column); + + $query = trim(Str::template($template, [ + 'name' => $this->quoteIdentifier($name), + 'null' => $null, + 'default' => $columnDefault['query'], + 'unique' => $uniqueColumn + ], ['fallback' => ''])); + + return [ + 'query' => $query, + 'bindings' => $columnDefault['bindings'], + 'key' => $column['key'] ?? null, + 'unique' => $uniqueKey + ]; + } + + /** + * Creates the inner query for the columns in a CREATE TABLE query + * + * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` + * @return array Array with a `query` string and `bindings`, `keys` and `unique` arrays + */ + public function createTableInner(array $columns): array + { + $query = []; + $bindings = []; + $keys = []; + $unique = []; + + foreach ($columns as $name => $column) { + $sql = $this->createColumn($name, $column); + + // collect query and bindings + $query[] = $sql['query']; + $bindings += $sql['bindings']; + + // make a list of keys per key name + if ($sql['key'] !== null) { + if (isset($keys[$sql['key']]) !== true) { + $keys[$sql['key']] = []; + } + + $keys[$sql['key']][] = $name; + if ($sql['unique'] === true) { + $unique[$sql['key']] = true; + } + } + } + + return [ + 'query' => implode(',' . PHP_EOL, $query), + 'bindings' => $bindings, + 'keys' => $keys, + 'unique' => $unique + ]; + } + + /** + * Creates a CREATE TABLE query + * + * @param string $table Table name + * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` + * @return array Array with a `query` string and a `bindings` array + */ + public function createTable(string $table, array $columns = []): array + { + $inner = $this->createTableInner($columns); + + // add keys + foreach ($inner['keys'] as $key => $columns) { + // quote each column name and make a list string out of the column names + $columns = implode(', ', array_map( + fn ($name) => $this->quoteIdentifier($name), + $columns + )); + + if ($key === 'primary') { + $key = 'PRIMARY KEY'; + } else { + $unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : ''; + $key = $unique . 'INDEX ' . $this->quoteIdentifier($key); + } + + $inner['query'] .= ',' . PHP_EOL . $key . ' (' . $columns . ')'; + } + + return [ + 'query' => 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')', + 'bindings' => $inner['bindings'] + ]; + } + + /** + * Builds a DELETE clause + * + * @param array $params List of parameters for the DELETE clause. See defaults for more info. + * @return array + */ + public function delete(array $params = []): array + { + $defaults = [ + 'table' => '', + 'where' => null, + 'bindings' => [] + ]; + + $options = array_merge($defaults, $params); + $bindings = $options['bindings']; + $query = ['DELETE']; + + // from + $this->extend($query, $bindings, $this->from($options['table'])); + + // where + $this->extend($query, $bindings, $this->where($options['where'])); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Creates the sql for dropping a single table + * + * @param string $table + * @return array + */ + public function dropTable(string $table): array + { + return [ + 'query' => 'DROP TABLE ' . $this->tableName($table), + 'bindings' => [] + ]; + } + + /** + * Extends a given query and bindings + * by reference + * + * @param array $query + * @param array $bindings + * @param array $input + * @return void + */ + public function extend(&$query, array &$bindings, $input) + { + if (empty($input['query']) === false) { + $query[] = $input['query']; + $bindings = array_merge($bindings, $input['bindings']); + } + } + + /** + * Creates the from syntax + * + * @param string $table + * @return array + */ + public function from(string $table): array + { + return [ + 'query' => 'FROM ' . $this->tableName($table), + 'bindings' => [] + ]; + } + + /** + * Creates the group by syntax + * + * @param string $group + * @return array + */ + public function group(string $group = null): array + { + if (empty($group) === true) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => 'GROUP BY ' . $group, + 'bindings' => [] + ]; + } + + /** + * Creates the having syntax + * + * @param string|null $having + * @return array + */ + public function having(string $having = null): array + { + if (empty($having) === true) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => 'HAVING ' . $having, + 'bindings' => [] + ]; + } + + /** + * Creates an insert query + * + * @param array $params + * @return array + */ + public function insert(array $params = []): array + { + $table = $params['table'] ?? null; + $values = $params['values'] ?? null; + $bindings = $params['bindings']; + $query = ['INSERT INTO ' . $this->tableName($table)]; + + // add the values + $this->extend($query, $bindings, $this->values($table, $values, ', ', false)); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Creates a join query + * + * @param string $table + * @param string $type + * @param string $on + * @return array + * @throws \Kirby\Exception\InvalidArgumentException if an invalid join type is given + */ + public function join(string $type, string $table, string $on): array + { + $types = [ + 'JOIN', + 'INNER JOIN', + 'OUTER JOIN', + 'LEFT OUTER JOIN', + 'LEFT JOIN', + 'RIGHT OUTER JOIN', + 'RIGHT JOIN', + 'FULL OUTER JOIN', + 'FULL JOIN', + 'NATURAL JOIN', + 'CROSS JOIN', + 'SELF JOIN' + ]; + + $type = strtoupper(trim($type)); + + // validate join type + if (in_array($type, $types) === false) { + throw new InvalidArgumentException('Invalid join type ' . $type); + } + + return [ + 'query' => $type . ' ' . $this->tableName($table) . ' ON ' . $on, + 'bindings' => [], + ]; + } + + /** + * Create the syntax for multiple joins + * + * @param array|null $joins + * @return array + */ + public function joins(array $joins = null): array + { + $query = []; + $bindings = []; + + foreach ((array)$joins as $join) { + $this->extend($query, $bindings, $this->join($join['type'] ?? 'JOIN', $join['table'] ?? null, $join['on'] ?? null)); + } + + return [ + 'query' => implode(' ', array_filter($query)), + 'bindings' => [], + ]; + } + + /** + * Creates a limit and offset query instruction + * + * @param int $offset + * @param int|null $limit + * @return array + */ + public function limit(int $offset = 0, int $limit = null): array + { + // no need to add it to the query + if ($offset === 0 && $limit === null) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + $limit ??= '18446744073709551615'; + + $offsetBinding = $this->bindingName('offset'); + $limitBinding = $this->bindingName('limit'); + + return [ + 'query' => 'LIMIT ' . $offsetBinding . ', ' . $limitBinding, + 'bindings' => [ + $limitBinding => $limit, + $offsetBinding => $offset, + ] + ]; + } + + /** + * Creates the order by syntax + * + * @param string $order + * @return array + */ + public function order(string $order = null): array + { + if (empty($order) === true) { + return [ + 'query' => null, + 'bindings' => [] + ]; + } + + return [ + 'query' => 'ORDER BY ' . $order, + 'bindings' => [] + ]; + } + + /** + * Converts a query array into a final string + * + * @param array $query + * @param string $separator + * @return string + */ + public function query(array $query, string $separator = ' ') + { + return implode($separator, array_filter($query)); + } + + /** + * Quotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + // * is special, don't quote that + if ($identifier === '*') { + return $identifier; + } + + // escape backticks inside the identifier name + $identifier = str_replace('`', '``', $identifier); + + // wrap in backticks + return '`' . $identifier . '`'; + } + + /** + * Builds a select clause + * + * @param array $params List of parameters for the select clause. Check out the defaults for more info. + * @return array An array with the query and the bindings + */ + public function select(array $params = []): array + { + $defaults = [ + 'table' => '', + 'columns' => '*', + 'join' => null, + 'distinct' => false, + 'where' => null, + 'group' => null, + 'having' => null, + 'order' => null, + 'offset' => 0, + 'limit' => null, + 'bindings' => [] + ]; + + $options = array_merge($defaults, $params); + $bindings = $options['bindings']; + $query = ['SELECT']; + + // select distinct values + if ($options['distinct'] === true) { + $query[] = 'DISTINCT'; + } + + // columns + $query[] = $this->selected($options['table'], $options['columns']); + + // from + $this->extend($query, $bindings, $this->from($options['table'])); + + // joins + $this->extend($query, $bindings, $this->joins($options['join'])); + + // where + $this->extend($query, $bindings, $this->where($options['where'])); + + // group + $this->extend($query, $bindings, $this->group($options['group'])); + + // having + $this->extend($query, $bindings, $this->having($options['having'])); + + // order + $this->extend($query, $bindings, $this->order($options['order'])); + + // offset and limit + $this->extend($query, $bindings, $this->limit($options['offset'], $options['limit'])); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Creates a columns definition from string or array + * + * @param string $table + * @param array|string|null $columns + * @return string + */ + public function selected($table, $columns = null): string + { + // all columns + if (empty($columns) === true) { + return '*'; + } + + // array of columns + if (is_array($columns) === true) { + // validate columns + $result = []; + + foreach ($columns as $column) { + list($table, $columnPart) = $this->splitIdentifier($table, $column); + + if ($this->validateColumn($table, $columnPart) === true) { + $result[] = $this->combineIdentifier($table, $columnPart); + } + } + + return implode(', ', $result); + } else { + return $columns; + } + } + + /** + * Splits a (qualified) identifier into table and column + * + * @param $table string Default table if the identifier is not qualified + * @param $identifier string + * @return array + * @throws \Kirby\Exception\InvalidArgumentException if an invalid identifier is given + */ + public function splitIdentifier($table, $identifier): array + { + // split by dot, but only outside of quotes + $parts = preg_split('/(?:`[^`]*`|"[^"]*")(*SKIP)(*F)|\./', $identifier); + + switch (count($parts)) { + case 1: + // non-qualified identifier + return [$table, $this->unquoteIdentifier($parts[0])]; + + case 2: + // qualified identifier + return [$this->unquoteIdentifier($parts[0]), $this->unquoteIdentifier($parts[1])]; + + default: + // every other number is an error + throw new InvalidArgumentException('Invalid identifier ' . $identifier); + } + } + + /** + * Returns a query to list the tables of the current database; + * the query needs to return rows with a column `name` + * + * @return array + */ + abstract public function tables(): array; + + /** + * Validates and quotes a table name + * + * @param string $table + * @return string + * @throws \Kirby\Exception\InvalidArgumentException if an invalid table name is given + */ + public function tableName(string $table): string + { + // validate table + if ($this->database->validateTable($table) === false) { + throw new InvalidArgumentException('Invalid table ' . $table); + } + + return $this->quoteIdentifier($table); + } + + /** + * Unquotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function unquoteIdentifier(string $identifier): string + { + // remove quotes around the identifier + if (in_array(Str::substr($identifier, 0, 1), ['"', '`']) === true) { + $identifier = Str::substr($identifier, 1); + } + + if (in_array(Str::substr($identifier, -1), ['"', '`']) === true) { + $identifier = Str::substr($identifier, 0, -1); + } + + // unescape duplicated quotes + return str_replace(['""', '``'], ['"', '`'], $identifier); + } + + /** + * Builds an update clause + * + * @param array $params List of parameters for the update clause. See defaults for more info. + * @return array + */ + public function update(array $params = []): array + { + $defaults = [ + 'table' => null, + 'values' => null, + 'where' => null, + 'bindings' => [] + ]; + + $options = array_merge($defaults, $params); + $bindings = $options['bindings']; + + // start the query + $query = ['UPDATE ' . $this->tableName($options['table']) . ' SET']; + + // add the values + $this->extend($query, $bindings, $this->values($options['table'], $options['values'])); + + // add the where clause + $this->extend($query, $bindings, $this->where($options['where'])); + + return [ + 'query' => $this->query($query), + 'bindings' => $bindings + ]; + } + + /** + * Validates a given column name in a table + * + * @param string $table + * @param string $column + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException If the column is invalid + */ + public function validateColumn(string $table, string $column): bool + { + if ($this->database->validateColumn($table, $column) !== true) { + throw new InvalidArgumentException('Invalid column ' . $column); + } + + return true; + } + + /** + * Builds a safe list of values for insert, select or update queries + * + * @param string $table Table name + * @param mixed $values A value string or array of values + * @param string $separator A separator which should be used to join values + * @param bool $set If true builds a set list of values for update clauses + * @param bool $enforceQualified Always use fully qualified column names + */ + public function values(string $table, $values, string $separator = ', ', bool $set = true, bool $enforceQualified = false): array + { + if (is_array($values) === false) { + return [ + 'query' => $values, + 'bindings' => [] + ]; + } + + if ($set === true) { + return $this->valueSet($table, $values, $separator, $enforceQualified); + } else { + return $this->valueList($table, $values, $separator, $enforceQualified); + } + } + + /** + * Creates a list of fields and values + * + * @param string $table + * @param string|array $values + * @param string $separator + * @param bool $enforceQualified + * @param array + */ + public function valueList(string $table, $values, string $separator = ',', bool $enforceQualified = false): array + { + $fields = []; + $query = []; + $bindings = []; + + foreach ($values as $column => $value) { + $key = $this->columnName($table, $column, $enforceQualified); + + if ($key === null) { + continue; + } + + $fields[] = $key; + + if (in_array($value, static::$literals, true) === true) { + $query[] = $value ?: 'null'; + continue; + } + + if (is_array($value) === true) { + $value = json_encode($value); + } + + // add the binding + $bindings[$bindingName = $this->bindingName('value')] = $value; + + // create the query + $query[] = $bindingName; + } + + return [ + 'query' => '(' . implode($separator, $fields) . ') VALUES (' . implode($separator, $query) . ')', + 'bindings' => $bindings + ]; + } + + /** + * Creates a set of values + * + * @param string $table + * @param string|array $values + * @param string $separator + * @param bool $enforceQualified + * @param array + * @return array + */ + public function valueSet(string $table, $values, string $separator = ',', bool $enforceQualified = false): array + { + $query = []; + $bindings = []; + + foreach ($values as $column => $value) { + $key = $this->columnName($table, $column, $enforceQualified); + + if ($key === null) { + continue; + } + + if (in_array($value, static::$literals, true) === true) { + $query[] = $key . ' = ' . ($value ?: 'null'); + continue; + } + + if (is_array($value) === true) { + $value = json_encode($value); + } + + // add the binding + $bindings[$bindingName = $this->bindingName('value')] = $value; + + // create the query + $query[] = $key . ' = ' . $bindingName; + } + + return [ + 'query' => implode($separator, $query), + 'bindings' => $bindings + ]; + } + + /** + * @param string|array|null $where + * @param array $bindings + * @return array + */ + public function where($where, array $bindings = []): array + { + if (empty($where) === true) { + return [ + 'query' => null, + 'bindings' => [], + ]; + } + + if (is_string($where) === true) { + return [ + 'query' => 'WHERE ' . $where, + 'bindings' => $bindings + ]; + } + + $query = []; + + foreach ($where as $key => $value) { + $binding = $this->bindingName('where_' . $key); + $bindings[$binding] = $value; + + $query[] = $key . ' = ' . $binding; + } + + return [ + 'query' => 'WHERE ' . implode(' AND ', $query), + 'bindings' => $bindings + ]; + } } diff --git a/kirby/src/Database/Sql/Mysql.php b/kirby/src/Database/Sql/Mysql.php index 1d351b5..f8fc9d3 100644 --- a/kirby/src/Database/Sql/Mysql.php +++ b/kirby/src/Database/Sql/Mysql.php @@ -15,45 +15,45 @@ use Kirby\Database\Sql; */ class Mysql extends Sql { - /** - * Returns a query to list the columns of a specified table; - * the query needs to return rows with a column `name` - * - * @param string $table Table name - * @return array - */ - public function columns(string $table): array - { - $databaseBinding = $this->bindingName('database'); - $tableBinding = $this->bindingName('table'); + /** + * Returns a query to list the columns of a specified table; + * the query needs to return rows with a column `name` + * + * @param string $table Table name + * @return array + */ + public function columns(string $table): array + { + $databaseBinding = $this->bindingName('database'); + $tableBinding = $this->bindingName('table'); - $query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS '; - $query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding; + $query = 'SELECT COLUMN_NAME AS name FROM INFORMATION_SCHEMA.COLUMNS '; + $query .= 'WHERE TABLE_SCHEMA = ' . $databaseBinding . ' AND TABLE_NAME = ' . $tableBinding; - return [ - 'query' => $query, - 'bindings' => [ - $databaseBinding => $this->database->name(), - $tableBinding => $table, - ] - ]; - } + return [ + 'query' => $query, + 'bindings' => [ + $databaseBinding => $this->database->name(), + $tableBinding => $table, + ] + ]; + } - /** - * Returns a query to list the tables of the current database; - * the query needs to return rows with a column `name` - * - * @return array - */ - public function tables(): array - { - $binding = $this->bindingName('database'); + /** + * Returns a query to list the tables of the current database; + * the query needs to return rows with a column `name` + * + * @return array + */ + public function tables(): array + { + $binding = $this->bindingName('database'); - return [ - 'query' => 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $binding, - 'bindings' => [ - $binding => $this->database->name() - ] - ]; - } + return [ + 'query' => 'SELECT TABLE_NAME AS name FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ' . $binding, + 'bindings' => [ + $binding => $this->database->name() + ] + ]; + } } diff --git a/kirby/src/Database/Sql/Sqlite.php b/kirby/src/Database/Sql/Sqlite.php index 5046c2c..c064522 100644 --- a/kirby/src/Database/Sql/Sqlite.php +++ b/kirby/src/Database/Sql/Sqlite.php @@ -15,130 +15,131 @@ use Kirby\Database\Sql; */ class Sqlite extends Sql { - /** - * Returns a query to list the columns of a specified table; - * the query needs to return rows with a column `name` - * - * @param string $table Table name - * @return array - */ - public function columns(string $table): array - { - return [ - 'query' => 'PRAGMA table_info(' . $this->tableName($table) . ')', - 'bindings' => [], - ]; - } + /** + * Returns a query to list the columns of a specified table; + * the query needs to return rows with a column `name` + * + * @param string $table Table name + * @return array + */ + public function columns(string $table): array + { + return [ + 'query' => 'PRAGMA table_info(' . $this->tableName($table) . ')', + 'bindings' => [], + ]; + } - /** - * Abstracted column types to simplify table - * creation for multiple database drivers - * @codeCoverageIgnore - * - * @return array - */ - public function columnTypes(): array - { - return [ - 'id' => '{{ name }} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE', - 'varchar' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}', - 'text' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}', - 'int' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}', - 'timestamp' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}' - ]; - } + /** + * Abstracted column types to simplify table + * creation for multiple database drivers + * @codeCoverageIgnore + * + * @return array + */ + public function columnTypes(): array + { + return [ + 'id' => '{{ name }} INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL UNIQUE', + 'varchar' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}', + 'text' => '{{ name }} TEXT {{ null }} {{ default }} {{ unique }}', + 'int' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}', + 'timestamp' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}', + 'bool' => '{{ name }} INTEGER {{ null }} {{ default }} {{ unique }}' + ]; + } - /** - * Combines an identifier (table and column) - * - * @param $table string - * @param $column string - * @param $values bool Whether the identifier is going to be used for a VALUES clause; - * only relevant for SQLite - * @return string - */ - public function combineIdentifier(string $table, string $column, bool $values = false): string - { - // SQLite doesn't support qualified column names for VALUES clauses - if ($values === true) { - return $this->quoteIdentifier($column); - } + /** + * Combines an identifier (table and column) + * + * @param $table string + * @param $column string + * @param $values bool Whether the identifier is going to be used for a VALUES clause; + * only relevant for SQLite + * @return string + */ + public function combineIdentifier(string $table, string $column, bool $values = false): string + { + // SQLite doesn't support qualified column names for VALUES clauses + if ($values === true) { + return $this->quoteIdentifier($column); + } - return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); - } + return $this->quoteIdentifier($table) . '.' . $this->quoteIdentifier($column); + } - /** - * Creates a CREATE TABLE query - * - * @param string $table Table name - * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` - * @return array Array with a `query` string and a `bindings` array - */ - public function createTable(string $table, array $columns = []): array - { - $inner = $this->createTableInner($columns); + /** + * Creates a CREATE TABLE query + * + * @param string $table Table name + * @param array $columns Array of column definition arrays, see `Kirby\Database\Sql::createColumn()` + * @return array Array with a `query` string and a `bindings` array + */ + public function createTable(string $table, array $columns = []): array + { + $inner = $this->createTableInner($columns); - // add keys - $keys = []; - foreach ($inner['keys'] as $key => $columns) { - // quote each column name and make a list string out of the column names - $columns = implode(', ', array_map( - fn ($name) => $this->quoteIdentifier($name), - $columns - )); + // add keys + $keys = []; + foreach ($inner['keys'] as $key => $columns) { + // quote each column name and make a list string out of the column names + $columns = implode(', ', array_map( + fn ($name) => $this->quoteIdentifier($name), + $columns + )); - if ($key === 'primary') { - $inner['query'] .= ',' . PHP_EOL . 'PRIMARY KEY (' . $columns . ')'; - } else { - // SQLite only supports index creation using a separate CREATE INDEX query - $unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : ''; - $keys[] = 'CREATE ' . $unique . 'INDEX ' . $this->quoteIdentifier($table . '_index_' . $key) . - ' ON ' . $this->quoteIdentifier($table) . ' (' . $columns . ')'; - } - } + if ($key === 'primary') { + $inner['query'] .= ',' . PHP_EOL . 'PRIMARY KEY (' . $columns . ')'; + } else { + // SQLite only supports index creation using a separate CREATE INDEX query + $unique = isset($inner['unique'][$key]) === true ? 'UNIQUE ' : ''; + $keys[] = 'CREATE ' . $unique . 'INDEX ' . $this->quoteIdentifier($table . '_index_' . $key) . + ' ON ' . $this->quoteIdentifier($table) . ' (' . $columns . ')'; + } + } - $query = 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')'; - if (empty($keys) === false) { - $query .= ';' . PHP_EOL . implode(';' . PHP_EOL, $keys); - } + $query = 'CREATE TABLE ' . $this->quoteIdentifier($table) . ' (' . PHP_EOL . $inner['query'] . PHP_EOL . ')'; + if (empty($keys) === false) { + $query .= ';' . PHP_EOL . implode(';' . PHP_EOL, $keys); + } - return [ - 'query' => $query, - 'bindings' => $inner['bindings'] - ]; - } + return [ + 'query' => $query, + 'bindings' => $inner['bindings'] + ]; + } - /** - * Quotes an identifier (table *or* column) - * - * @param $identifier string - * @return string - */ - public function quoteIdentifier(string $identifier): string - { - // * is special - if ($identifier === '*') { - return $identifier; - } + /** + * Quotes an identifier (table *or* column) + * + * @param $identifier string + * @return string + */ + public function quoteIdentifier(string $identifier): string + { + // * is special + if ($identifier === '*') { + return $identifier; + } - // escape quotes inside the identifier name - $identifier = str_replace('"', '""', $identifier); + // escape quotes inside the identifier name + $identifier = str_replace('"', '""', $identifier); - // wrap in quotes - return '"' . $identifier . '"'; - } + // wrap in quotes + return '"' . $identifier . '"'; + } - /** - * Returns a query to list the tables of the current database; - * the query needs to return rows with a column `name` - * - * @return string - */ - public function tables(): array - { - return [ - 'query' => 'SELECT name FROM sqlite_master WHERE type = "table"', - 'bindings' => [] - ]; - } + /** + * Returns a query to list the tables of the current database; + * the query needs to return rows with a column `name` + * + * @return string + */ + public function tables(): array + { + return [ + 'query' => 'SELECT name FROM sqlite_master WHERE type = "table" OR type = "view"', + 'bindings' => [] + ]; + } } diff --git a/kirby/src/Email/Body.php b/kirby/src/Email/Body.php index 403a5fa..716dfd5 100644 --- a/kirby/src/Email/Body.php +++ b/kirby/src/Email/Body.php @@ -17,69 +17,69 @@ use Kirby\Toolkit\Properties; */ class Body { - use Properties; + use Properties; - /** - * @var string - */ - protected $html; + /** + * @var string + */ + protected $html; - /** - * @var string - */ - protected $text; + /** + * @var string + */ + protected $text; - /** - * Email body constructor - * - * @param array $props - */ - public function __construct(array $props = []) - { - $this->setProperties($props); - } + /** + * Email body constructor + * + * @param array $props + */ + public function __construct(array $props = []) + { + $this->setProperties($props); + } - /** - * Returns the HTML content of the email body - * - * @return string - */ - public function html() - { - return $this->html ?? ''; - } + /** + * Returns the HTML content of the email body + * + * @return string + */ + public function html() + { + return $this->html ?? ''; + } - /** - * Returns the plain text content of the email body - * - * @return string - */ - public function text() - { - return $this->text ?? ''; - } + /** + * Returns the plain text content of the email body + * + * @return string + */ + public function text() + { + return $this->text ?? ''; + } - /** - * Sets the HTML content for the email body - * - * @param string|null $html - * @return $this - */ - protected function setHtml(string $html = null) - { - $this->html = $html; - return $this; - } + /** + * Sets the HTML content for the email body + * + * @param string|null $html + * @return $this + */ + protected function setHtml(string $html = null) + { + $this->html = $html; + return $this; + } - /** - * Sets the plain text content for the email body - * - * @param string|null $text - * @return $this - */ - protected function setText(string $text = null) - { - $this->text = $text; - return $this; - } + /** + * Sets the plain text content for the email body + * + * @param string|null $text + * @return $this + */ + protected function setText(string $text = null) + { + $this->text = $text; + return $this; + } } diff --git a/kirby/src/Email/Email.php b/kirby/src/Email/Email.php index 3f96793..5cacbb7 100644 --- a/kirby/src/Email/Email.php +++ b/kirby/src/Email/Email.php @@ -19,454 +19,454 @@ use Kirby\Toolkit\V; */ class Email { - use Properties; + use Properties; - /** - * If set to `true`, the debug mode is enabled - * for all emails - * - * @var bool - */ - public static $debug = false; + /** + * If set to `true`, the debug mode is enabled + * for all emails + * + * @var bool + */ + public static $debug = false; - /** - * Store for sent emails when `Email::$debug` - * is set to `true` - * - * @var array - */ - public static $emails = []; + /** + * Store for sent emails when `Email::$debug` + * is set to `true` + * + * @var array + */ + public static $emails = []; - /** - * @var array|null - */ - protected $attachments; + /** + * @var array|null + */ + protected $attachments; - /** - * @var \Kirby\Email\Body|null - */ - protected $body; + /** + * @var \Kirby\Email\Body|null + */ + protected $body; - /** - * @var array|null - */ - protected $bcc; + /** + * @var array|null + */ + protected $bcc; - /** - * @var \Closure|null - */ - protected $beforeSend; + /** + * @var \Closure|null + */ + protected $beforeSend; - /** - * @var array|null - */ - protected $cc; + /** + * @var array|null + */ + protected $cc; - /** - * @var string|null - */ - protected $from; + /** + * @var string|null + */ + protected $from; - /** - * @var string|null - */ - protected $fromName; + /** + * @var string|null + */ + protected $fromName; - /** - * @var string|null - */ - protected $replyTo; + /** + * @var string|null + */ + protected $replyTo; - /** - * @var string|null - */ - protected $replyToName; + /** + * @var string|null + */ + protected $replyToName; - /** - * @var bool - */ - protected $isSent = false; + /** + * @var bool + */ + protected $isSent = false; - /** - * @var string|null - */ - protected $subject; + /** + * @var string|null + */ + protected $subject; - /** - * @var array|null - */ - protected $to; + /** + * @var array|null + */ + protected $to; - /** - * @var array|null - */ - protected $transport; + /** + * @var array|null + */ + protected $transport; - /** - * Email constructor - * - * @param array $props - * @param bool $debug - */ - public function __construct(array $props = [], bool $debug = false) - { - $this->setProperties($props); + /** + * Email constructor + * + * @param array $props + * @param bool $debug + */ + public function __construct(array $props = [], bool $debug = false) + { + $this->setProperties($props); - // @codeCoverageIgnoreStart - if (static::$debug === false && $debug === false) { - $this->send(); - } elseif (static::$debug === true) { - static::$emails[] = $this; - } - // @codeCoverageIgnoreEnd - } + // @codeCoverageIgnoreStart + if (static::$debug === false && $debug === false) { + $this->send(); + } elseif (static::$debug === true) { + static::$emails[] = $this; + } + // @codeCoverageIgnoreEnd + } - /** - * Returns the email attachments - * - * @return array - */ - public function attachments(): array - { - return $this->attachments; - } + /** + * Returns the email attachments + * + * @return array + */ + public function attachments(): array + { + return $this->attachments; + } - /** - * Returns the email body - * - * @return \Kirby\Email\Body|null - */ - public function body() - { - return $this->body; - } + /** + * Returns the email body + * + * @return \Kirby\Email\Body|null + */ + public function body() + { + return $this->body; + } - /** - * Returns "bcc" recipients - * - * @return array - */ - public function bcc(): array - { - return $this->bcc; - } + /** + * Returns "bcc" recipients + * + * @return array + */ + public function bcc(): array + { + return $this->bcc; + } - /** - * Returns the beforeSend callback closure, - * which has access to the PHPMailer instance - * - * @return \Closure|null - */ - public function beforeSend(): ?Closure - { - return $this->beforeSend; - } + /** + * Returns the beforeSend callback closure, + * which has access to the PHPMailer instance + * + * @return \Closure|null + */ + public function beforeSend(): ?Closure + { + return $this->beforeSend; + } - /** - * Returns "cc" recipients - * - * @return array - */ - public function cc(): array - { - return $this->cc; - } + /** + * Returns "cc" recipients + * + * @return array + */ + public function cc(): array + { + return $this->cc; + } - /** - * Returns default transport settings - * - * @return array - */ - protected function defaultTransport(): array - { - return [ - 'type' => 'mail' - ]; - } + /** + * Returns default transport settings + * + * @return array + */ + protected function defaultTransport(): array + { + return [ + 'type' => 'mail' + ]; + } - /** - * Returns the "from" email address - * - * @return string - */ - public function from(): string - { - return $this->from; - } + /** + * Returns the "from" email address + * + * @return string + */ + public function from(): string + { + return $this->from; + } - /** - * Returns the "from" name - * - * @return string|null - */ - public function fromName(): ?string - { - return $this->fromName; - } + /** + * Returns the "from" name + * + * @return string|null + */ + public function fromName(): ?string + { + return $this->fromName; + } - /** - * Checks if the email has an HTML body - * - * @return bool - */ - public function isHtml() - { - return empty($this->body()->html()) === false; - } + /** + * Checks if the email has an HTML body + * + * @return bool + */ + public function isHtml() + { + return empty($this->body()->html()) === false; + } - /** - * Checks if the email has been sent successfully - * - * @return bool - */ - public function isSent(): bool - { - return $this->isSent; - } + /** + * Checks if the email has been sent successfully + * + * @return bool + */ + public function isSent(): bool + { + return $this->isSent; + } - /** - * Returns the "reply to" email address - * - * @return string - */ - public function replyTo(): string - { - return $this->replyTo; - } + /** + * Returns the "reply to" email address + * + * @return string + */ + public function replyTo(): string + { + return $this->replyTo; + } - /** - * Returns the "reply to" name - * - * @return string|null - */ - public function replyToName(): ?string - { - return $this->replyToName; - } + /** + * Returns the "reply to" name + * + * @return string|null + */ + public function replyToName(): ?string + { + return $this->replyToName; + } - /** - * Converts single or multiple email addresses to a sanitized format - * - * @param string|array|null $email - * @param bool $multiple - * @return array|mixed|string - * @throws \Exception - */ - protected function resolveEmail($email = null, bool $multiple = true) - { - if ($email === null) { - return $multiple === true ? [] : ''; - } + /** + * Converts single or multiple email addresses to a sanitized format + * + * @param string|array|null $email + * @param bool $multiple + * @return array|mixed|string + * @throws \Exception + */ + protected function resolveEmail($email = null, bool $multiple = true) + { + if ($email === null) { + return $multiple === true ? [] : ''; + } - if (is_array($email) === false) { - $email = [$email => null]; - } + if (is_array($email) === false) { + $email = [$email => null]; + } - $result = []; - foreach ($email as $address => $name) { - // convert simple email arrays to associative arrays - if (is_int($address) === true) { - // the value is the address, there is no name - $address = $name; - $result[$address] = null; - } else { - $result[$address] = $name; - } + $result = []; + foreach ($email as $address => $name) { + // convert simple email arrays to associative arrays + if (is_int($address) === true) { + // the value is the address, there is no name + $address = $name; + $result[$address] = null; + } else { + $result[$address] = $name; + } - // ensure that the address is valid - if (V::email($address) === false) { - throw new Exception(sprintf('"%s" is not a valid email address', $address)); - } - } + // ensure that the address is valid + if (V::email($address) === false) { + throw new Exception(sprintf('"%s" is not a valid email address', $address)); + } + } - return $multiple === true ? $result : array_keys($result)[0]; - } + return $multiple === true ? $result : array_keys($result)[0]; + } - /** - * Sends the email - * - * @return bool - */ - public function send(): bool - { - return $this->isSent = true; - } + /** + * Sends the email + * + * @return bool + */ + public function send(): bool + { + return $this->isSent = true; + } - /** - * Sets the email attachments - * - * @param array|null $attachments - * @return $this - */ - protected function setAttachments($attachments = null) - { - $this->attachments = $attachments ?? []; - return $this; - } + /** + * Sets the email attachments + * + * @param array|null $attachments + * @return $this + */ + protected function setAttachments($attachments = null) + { + $this->attachments = $attachments ?? []; + return $this; + } - /** - * Sets the email body - * - * @param string|array $body - * @return $this - */ - protected function setBody($body) - { - if (is_string($body) === true) { - $body = ['text' => $body]; - } + /** + * Sets the email body + * + * @param string|array $body + * @return $this + */ + protected function setBody($body) + { + if (is_string($body) === true) { + $body = ['text' => $body]; + } - $this->body = new Body($body); - return $this; - } + $this->body = new Body($body); + return $this; + } - /** - * Sets "bcc" recipients - * - * @param string|array|null $bcc - * @return $this - */ - protected function setBcc($bcc = null) - { - $this->bcc = $this->resolveEmail($bcc); - return $this; - } + /** + * Sets "bcc" recipients + * + * @param string|array|null $bcc + * @return $this + */ + protected function setBcc($bcc = null) + { + $this->bcc = $this->resolveEmail($bcc); + return $this; + } - /** - * Sets the "beforeSend" callback - * - * @param \Closure|null $beforeSend - * @return $this - */ - protected function setBeforeSend(?Closure $beforeSend = null) - { - $this->beforeSend = $beforeSend; - return $this; - } + /** + * Sets the "beforeSend" callback + * + * @param \Closure|null $beforeSend + * @return $this + */ + protected function setBeforeSend(?Closure $beforeSend = null) + { + $this->beforeSend = $beforeSend; + return $this; + } - /** - * Sets "cc" recipients - * - * @param string|array|null $cc - * @return $this - */ - protected function setCc($cc = null) - { - $this->cc = $this->resolveEmail($cc); - return $this; - } + /** + * Sets "cc" recipients + * + * @param string|array|null $cc + * @return $this + */ + protected function setCc($cc = null) + { + $this->cc = $this->resolveEmail($cc); + return $this; + } - /** - * Sets the "from" email address - * - * @param string $from - * @return $this - */ - protected function setFrom(string $from) - { - $this->from = $this->resolveEmail($from, false); - return $this; - } + /** + * Sets the "from" email address + * + * @param string $from + * @return $this + */ + protected function setFrom(string $from) + { + $this->from = $this->resolveEmail($from, false); + return $this; + } - /** - * Sets the "from" name - * - * @param string|null $fromName - * @return $this - */ - protected function setFromName(string $fromName = null) - { - $this->fromName = $fromName; - return $this; - } + /** + * Sets the "from" name + * + * @param string|null $fromName + * @return $this + */ + protected function setFromName(string $fromName = null) + { + $this->fromName = $fromName; + return $this; + } - /** - * Sets the "reply to" email address - * - * @param string|null $replyTo - * @return $this - */ - protected function setReplyTo(string $replyTo = null) - { - $this->replyTo = $this->resolveEmail($replyTo, false); - return $this; - } + /** + * Sets the "reply to" email address + * + * @param string|null $replyTo + * @return $this + */ + protected function setReplyTo(string $replyTo = null) + { + $this->replyTo = $this->resolveEmail($replyTo, false); + return $this; + } - /** - * Sets the "reply to" name - * - * @param string|null $replyToName - * @return $this - */ - protected function setReplyToName(string $replyToName = null) - { - $this->replyToName = $replyToName; - return $this; - } + /** + * Sets the "reply to" name + * + * @param string|null $replyToName + * @return $this + */ + protected function setReplyToName(string $replyToName = null) + { + $this->replyToName = $replyToName; + return $this; + } - /** - * Sets the email subject - * - * @param string $subject - * @return $this - */ - protected function setSubject(string $subject) - { - $this->subject = $subject; - return $this; - } + /** + * Sets the email subject + * + * @param string $subject + * @return $this + */ + protected function setSubject(string $subject) + { + $this->subject = $subject; + return $this; + } - /** - * Sets the recipients of the email - * - * @param string|array $to - * @return $this - */ - protected function setTo($to) - { - $this->to = $this->resolveEmail($to); - return $this; - } + /** + * Sets the recipients of the email + * + * @param string|array $to + * @return $this + */ + protected function setTo($to) + { + $this->to = $this->resolveEmail($to); + return $this; + } - /** - * Sets the email transport settings - * - * @param array|null $transport - * @return $this - */ - protected function setTransport($transport = null) - { - $this->transport = $transport; - return $this; - } + /** + * Sets the email transport settings + * + * @param array|null $transport + * @return $this + */ + protected function setTransport($transport = null) + { + $this->transport = $transport; + return $this; + } - /** - * Returns the email subject - * - * @return string - */ - public function subject(): string - { - return $this->subject; - } + /** + * Returns the email subject + * + * @return string + */ + public function subject(): string + { + return $this->subject; + } - /** - * Returns the email recipients - * - * @return array - */ - public function to(): array - { - return $this->to; - } + /** + * Returns the email recipients + * + * @return array + */ + public function to(): array + { + return $this->to; + } - /** - * Returns the email transports settings - * - * @return array - */ - public function transport(): array - { - return $this->transport ?? $this->defaultTransport(); - } + /** + * Returns the email transports settings + * + * @return array + */ + public function transport(): array + { + return $this->transport ?? $this->defaultTransport(); + } } diff --git a/kirby/src/Email/PHPMailer.php b/kirby/src/Email/PHPMailer.php index cc0ba8c..17f89cf 100644 --- a/kirby/src/Email/PHPMailer.php +++ b/kirby/src/Email/PHPMailer.php @@ -17,97 +17,97 @@ use PHPMailer\PHPMailer\PHPMailer as Mailer; */ class PHPMailer extends Email { - /** - * Sends email via PHPMailer library - * - * @param bool $debug - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function send(bool $debug = false): bool - { - $mailer = new Mailer(true); + /** + * Sends email via PHPMailer library + * + * @param bool $debug + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function send(bool $debug = false): bool + { + $mailer = new Mailer(true); - // set sender's address - $mailer->setFrom($this->from(), $this->fromName() ?? ''); + // set sender's address + $mailer->setFrom($this->from(), $this->fromName() ?? ''); - // optional reply-to address - if ($replyTo = $this->replyTo()) { - $mailer->addReplyTo($replyTo, $this->replyToName() ?? ''); - } + // optional reply-to address + if ($replyTo = $this->replyTo()) { + $mailer->addReplyTo($replyTo, $this->replyToName() ?? ''); + } - // add (multiple) recipient, CC & BCC addresses - foreach ($this->to() as $email => $name) { - $mailer->addAddress($email, $name ?? ''); - } - foreach ($this->cc() as $email => $name) { - $mailer->addCC($email, $name ?? ''); - } - foreach ($this->bcc() as $email => $name) { - $mailer->addBCC($email, $name ?? ''); - } + // add (multiple) recipient, CC & BCC addresses + foreach ($this->to() as $email => $name) { + $mailer->addAddress($email, $name ?? ''); + } + foreach ($this->cc() as $email => $name) { + $mailer->addCC($email, $name ?? ''); + } + foreach ($this->bcc() as $email => $name) { + $mailer->addBCC($email, $name ?? ''); + } - $mailer->Subject = $this->subject(); - $mailer->CharSet = 'UTF-8'; + $mailer->Subject = $this->subject(); + $mailer->CharSet = 'UTF-8'; - // set body according to html/text - if ($this->isHtml()) { - $mailer->isHTML(true); - $mailer->Body = $this->body()->html(); - $mailer->AltBody = $this->body()->text(); - } else { - $mailer->Body = $this->body()->text(); - } + // set body according to html/text + if ($this->isHtml()) { + $mailer->isHTML(true); + $mailer->Body = $this->body()->html(); + $mailer->AltBody = $this->body()->text(); + } else { + $mailer->Body = $this->body()->text(); + } - // add attachments - foreach ($this->attachments() as $attachment) { - $mailer->addAttachment($attachment); - } + // add attachments + foreach ($this->attachments() as $attachment) { + $mailer->addAttachment($attachment); + } - // smtp transport settings - if (($this->transport()['type'] ?? 'mail') === 'smtp') { - $mailer->isSMTP(); - $mailer->Host = $this->transport()['host'] ?? null; - $mailer->SMTPAuth = $this->transport()['auth'] ?? false; - $mailer->Username = $this->transport()['username'] ?? null; - $mailer->Password = $this->transport()['password'] ?? null; - $mailer->SMTPSecure = $this->transport()['security'] ?? 'ssl'; - $mailer->Port = $this->transport()['port'] ?? null; + // smtp transport settings + if (($this->transport()['type'] ?? 'mail') === 'smtp') { + $mailer->isSMTP(); + $mailer->Host = $this->transport()['host'] ?? null; + $mailer->SMTPAuth = $this->transport()['auth'] ?? false; + $mailer->Username = $this->transport()['username'] ?? null; + $mailer->Password = $this->transport()['password'] ?? null; + $mailer->SMTPSecure = $this->transport()['security'] ?? 'ssl'; + $mailer->Port = $this->transport()['port'] ?? null; - if ($mailer->SMTPSecure === true) { - switch ($mailer->Port) { - case null: - case 587: - $mailer->SMTPSecure = 'tls'; - $mailer->Port = 587; - break; - case 465: - $mailer->SMTPSecure = 'ssl'; - break; - default: - throw new InvalidArgumentException( - 'Could not automatically detect the "security" protocol from the ' . - '"port" option, please set it explicitly to "tls" or "ssl".' - ); - } - } - } + if ($mailer->SMTPSecure === true) { + switch ($mailer->Port) { + case null: + case 587: + $mailer->SMTPSecure = 'tls'; + $mailer->Port = 587; + break; + case 465: + $mailer->SMTPSecure = 'ssl'; + break; + default: + throw new InvalidArgumentException( + 'Could not automatically detect the "security" protocol from the ' . + '"port" option, please set it explicitly to "tls" or "ssl".' + ); + } + } + } - // accessible phpMailer instance - $beforeSend = $this->beforeSend(); + // accessible phpMailer instance + $beforeSend = $this->beforeSend(); - if (empty($beforeSend) === false && is_a($beforeSend, 'Closure') === true) { - $mailer = $beforeSend->call($this, $mailer) ?? $mailer; + if (empty($beforeSend) === false && is_a($beforeSend, 'Closure') === true) { + $mailer = $beforeSend->call($this, $mailer) ?? $mailer; - if (is_a($mailer, 'PHPMailer\PHPMailer\PHPMailer') === false) { - throw new InvalidArgumentException('"beforeSend" option return should be instance of PHPMailer\PHPMailer\PHPMailer class'); - } - } + if (is_a($mailer, 'PHPMailer\PHPMailer\PHPMailer') === false) { + throw new InvalidArgumentException('"beforeSend" option return should be instance of PHPMailer\PHPMailer\PHPMailer class'); + } + } - if ($debug === true) { - return $this->isSent = true; - } + if ($debug === true) { + return $this->isSent = true; + } - return $this->isSent = $mailer->send(); // @codeCoverageIgnore - } + return $this->isSent = $mailer->send(); // @codeCoverageIgnore + } } diff --git a/kirby/src/Exception/BadMethodCallException.php b/kirby/src/Exception/BadMethodCallException.php index a3b2c62..68c1f05 100644 --- a/kirby/src/Exception/BadMethodCallException.php +++ b/kirby/src/Exception/BadMethodCallException.php @@ -14,8 +14,8 @@ namespace Kirby\Exception; */ class BadMethodCallException extends Exception { - protected static $defaultKey = 'invalidMethod'; - protected static $defaultFallback = 'The method "{ method }" does not exist'; - protected static $defaultHttpCode = 400; - protected static $defaultData = ['method' => null]; + protected static $defaultKey = 'invalidMethod'; + protected static $defaultFallback = 'The method "{ method }" does not exist'; + protected static $defaultHttpCode = 400; + protected static $defaultData = ['method' => null]; } diff --git a/kirby/src/Exception/DuplicateException.php b/kirby/src/Exception/DuplicateException.php index b57f636..74bc859 100644 --- a/kirby/src/Exception/DuplicateException.php +++ b/kirby/src/Exception/DuplicateException.php @@ -15,7 +15,7 @@ namespace Kirby\Exception; */ class DuplicateException extends Exception { - protected static $defaultKey = 'duplicate'; - protected static $defaultFallback = 'The entry exists'; - protected static $defaultHttpCode = 400; + protected static $defaultKey = 'duplicate'; + protected static $defaultFallback = 'The entry exists'; + protected static $defaultHttpCode = 400; } diff --git a/kirby/src/Exception/ErrorPageException.php b/kirby/src/Exception/ErrorPageException.php index a1d83d8..7e9ed69 100644 --- a/kirby/src/Exception/ErrorPageException.php +++ b/kirby/src/Exception/ErrorPageException.php @@ -15,7 +15,7 @@ namespace Kirby\Exception; */ class ErrorPageException extends Exception { - protected static $defaultKey = 'errorPage'; - protected static $defaultFallback = 'Triggered error page'; - protected static $defaultHttpCode = 404; + protected static $defaultKey = 'errorPage'; + protected static $defaultFallback = 'Triggered error page'; + protected static $defaultHttpCode = 404; } diff --git a/kirby/src/Exception/Exception.php b/kirby/src/Exception/Exception.php index 596b49d..d3a0ba9 100644 --- a/kirby/src/Exception/Exception.php +++ b/kirby/src/Exception/Exception.php @@ -2,6 +2,7 @@ namespace Kirby\Exception; +use Kirby\Http\Environment; use Kirby\Toolkit\I18n; use Kirby\Toolkit\Str; @@ -18,208 +19,209 @@ use Kirby\Toolkit\Str; */ class Exception extends \Exception { - /** - * Data variables that can be used inside the exception message - * - * @var array - */ - protected $data; + /** + * Data variables that can be used inside the exception message + * + * @var array + */ + protected $data; - /** - * HTTP code that corresponds with the exception - * - * @var int - */ - protected $httpCode; + /** + * HTTP code that corresponds with the exception + * + * @var int + */ + protected $httpCode; - /** - * Additional details that are not included in the exception message - * - * @var array - */ - protected $details; + /** + * Additional details that are not included in the exception message + * + * @var array + */ + protected $details; - /** - * Whether the exception message could be translated into the user's language - * - * @var bool - */ - protected $isTranslated = true; + /** + * Whether the exception message could be translated into the user's language + * + * @var bool + */ + protected $isTranslated = true; - /** - * Defaults that can be overridden by specific - * exception classes - */ - protected static $defaultKey = 'general'; - protected static $defaultFallback = 'An error occurred'; - protected static $defaultData = []; - protected static $defaultHttpCode = 500; - protected static $defaultDetails = []; + /** + * Defaults that can be overridden by specific + * exception classes + */ + protected static $defaultKey = 'general'; + protected static $defaultFallback = 'An error occurred'; + protected static $defaultData = []; + protected static $defaultHttpCode = 500; + protected static $defaultDetails = []; - /** - * Prefix for the exception key (e.g. 'error.general') - * - * @var string - */ - private static $prefix = 'error'; + /** + * Prefix for the exception key (e.g. 'error.general') + * + * @var string + */ + private static $prefix = 'error'; - /** - * Class constructor - * - * @param array|string $args Full option array ('key', 'translate', 'fallback', - * 'data', 'httpCode', 'details' and 'previous') or - * just the message string - */ - public function __construct($args = []) - { - // set data and httpCode from provided arguments or defaults - $this->data = $args['data'] ?? static::$defaultData; - $this->httpCode = $args['httpCode'] ?? static::$defaultHttpCode; - $this->details = $args['details'] ?? static::$defaultDetails; + /** + * Class constructor + * + * @param array|string $args Full option array ('key', 'translate', 'fallback', + * 'data', 'httpCode', 'details' and 'previous') or + * just the message string + */ + public function __construct($args = []) + { + // set data and httpCode from provided arguments or defaults + $this->data = $args['data'] ?? static::$defaultData; + $this->httpCode = $args['httpCode'] ?? static::$defaultHttpCode; + $this->details = $args['details'] ?? static::$defaultDetails; - // define the Exception key - $key = self::$prefix . '.' . ($args['key'] ?? static::$defaultKey); + // define the Exception key + $key = self::$prefix . '.' . ($args['key'] ?? static::$defaultKey); - if (is_string($args) === true) { - $this->isTranslated = false; - parent::__construct($args); - } else { - // define whether message can/should be translated - $translate = ($args['translate'] ?? true) === true && class_exists('Kirby\Cms\App') === true; + if (is_string($args) === true) { + $this->isTranslated = false; + parent::__construct($args); + } else { + // define whether message can/should be translated + $translate = ($args['translate'] ?? true) === true && class_exists('Kirby\Cms\App') === true; - // fallback waterfall for message string - $message = null; + // fallback waterfall for message string + $message = null; - if ($translate) { - // 1. translation for provided key in current language - // 2. translation for provided key in default language - if (isset($args['key']) === true) { - $message = I18n::translate(self::$prefix . '.' . $args['key']); - $this->isTranslated = true; - } - } + if ($translate) { + // 1. translation for provided key in current language + // 2. translation for provided key in default language + if (isset($args['key']) === true) { + $message = I18n::translate(self::$prefix . '.' . $args['key']); + $this->isTranslated = true; + } + } - // 3. provided fallback message - if ($message === null) { - $message = $args['fallback'] ?? null; - $this->isTranslated = false; - } + // 3. provided fallback message + if ($message === null) { + $message = $args['fallback'] ?? null; + $this->isTranslated = false; + } - if ($translate) { - // 4. translation for default key in current language - // 5. translation for default key in default language - if ($message === null) { - $message = I18n::translate(self::$prefix . '.' . static::$defaultKey); - $this->isTranslated = true; - } - } + if ($translate) { + // 4. translation for default key in current language + // 5. translation for default key in default language + if ($message === null) { + $message = I18n::translate(self::$prefix . '.' . static::$defaultKey); + $this->isTranslated = true; + } + } - // 6. default fallback message - if ($message === null) { - $message = static::$defaultFallback; - $this->isTranslated = false; - } + // 6. default fallback message + if ($message === null) { + $message = static::$defaultFallback; + $this->isTranslated = false; + } - // format message with passed data - $message = Str::template($message, $this->data, [ - 'fallback' => '-', - 'start' => '{', - 'end' => '}' - ]); + // format message with passed data + $message = Str::template($message, $this->data, [ + 'fallback' => '-', + 'start' => '{', + 'end' => '}' + ]); - // handover to Exception parent class constructor - parent::__construct($message, 0, $args['previous'] ?? null); - } + // handover to Exception parent class constructor + parent::__construct($message, 0, $args['previous'] ?? null); + } - // set the Exception code to the key - $this->code = $key; - } + // set the Exception code to the key + $this->code = $key; + } - /** - * Returns the file in which the Exception was created - * relative to the document root - * - * @return string - */ - final public function getFileRelative(): string - { - $file = $this->getFile(); + /** + * Returns the file in which the Exception was created + * relative to the document root + * + * @return string + */ + final public function getFileRelative(): string + { + $file = $this->getFile(); + $docRoot = Environment::getGlobally('DOCUMENT_ROOT'); - if (empty($_SERVER['DOCUMENT_ROOT']) === false) { - $file = ltrim(Str::after($file, $_SERVER['DOCUMENT_ROOT']), '/'); - } + if (empty($docRoot) === false) { + $file = ltrim(Str::after($file, $docRoot), '/'); + } - return $file; - } + return $file; + } - /** - * Returns the data variables from the message - * - * @return array - */ - final public function getData(): array - { - return $this->data; - } + /** + * Returns the data variables from the message + * + * @return array + */ + final public function getData(): array + { + return $this->data; + } - /** - * Returns the additional details that are - * not included in the message - * - * @return array - */ - final public function getDetails(): array - { - return $this->details; - } + /** + * Returns the additional details that are + * not included in the message + * + * @return array + */ + final public function getDetails(): array + { + return $this->details; + } - /** - * Returns the exception key (error type) - * - * @return string - */ - final public function getKey(): string - { - return $this->getCode(); - } + /** + * Returns the exception key (error type) + * + * @return string + */ + final public function getKey(): string + { + return $this->getCode(); + } - /** - * Returns the HTTP code that corresponds - * with the exception - * - * @return array - */ - final public function getHttpCode(): int - { - return $this->httpCode; - } + /** + * Returns the HTTP code that corresponds + * with the exception + * + * @return array + */ + final public function getHttpCode(): int + { + return $this->httpCode; + } - /** - * Returns whether the exception message could - * be translated into the user's language - * - * @return bool - */ - final public function isTranslated(): bool - { - return $this->isTranslated; - } + /** + * Returns whether the exception message could + * be translated into the user's language + * + * @return bool + */ + final public function isTranslated(): bool + { + return $this->isTranslated; + } - /** - * Converts the object to an array - * - * @return array - */ - public function toArray(): array - { - return [ - 'exception' => static::class, - 'message' => $this->getMessage(), - 'key' => $this->getKey(), - 'file' => $this->getFileRelative(), - 'line' => $this->getLine(), - 'details' => $this->getDetails(), - 'code' => $this->getHttpCode() - ]; - } + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + return [ + 'exception' => static::class, + 'message' => $this->getMessage(), + 'key' => $this->getKey(), + 'file' => $this->getFileRelative(), + 'line' => $this->getLine(), + 'details' => $this->getDetails(), + 'code' => $this->getHttpCode() + ]; + } } diff --git a/kirby/src/Exception/InvalidArgumentException.php b/kirby/src/Exception/InvalidArgumentException.php index 874a522..d6cef17 100644 --- a/kirby/src/Exception/InvalidArgumentException.php +++ b/kirby/src/Exception/InvalidArgumentException.php @@ -14,8 +14,8 @@ namespace Kirby\Exception; */ class InvalidArgumentException extends Exception { - protected static $defaultKey = 'invalidArgument'; - protected static $defaultFallback = 'Invalid argument "{ argument }" in method "{ method }"'; - protected static $defaultHttpCode = 400; - protected static $defaultData = ['argument' => null, 'method' => null]; + protected static $defaultKey = 'invalidArgument'; + protected static $defaultFallback = 'Invalid argument "{ argument }" in method "{ method }"'; + protected static $defaultHttpCode = 400; + protected static $defaultData = ['argument' => null, 'method' => null]; } diff --git a/kirby/src/Exception/LogicException.php b/kirby/src/Exception/LogicException.php index 656c9bb..d1fbc55 100644 --- a/kirby/src/Exception/LogicException.php +++ b/kirby/src/Exception/LogicException.php @@ -14,7 +14,7 @@ namespace Kirby\Exception; */ class LogicException extends Exception { - protected static $defaultKey = 'logic'; - protected static $defaultFallback = 'This task cannot be finished'; - protected static $defaultHttpCode = 400; + protected static $defaultKey = 'logic'; + protected static $defaultFallback = 'This task cannot be finished'; + protected static $defaultHttpCode = 400; } diff --git a/kirby/src/Exception/NotFoundException.php b/kirby/src/Exception/NotFoundException.php index 2f84438..1f48c4b 100644 --- a/kirby/src/Exception/NotFoundException.php +++ b/kirby/src/Exception/NotFoundException.php @@ -14,7 +14,7 @@ namespace Kirby\Exception; */ class NotFoundException extends Exception { - protected static $defaultKey = 'notFound'; - protected static $defaultFallback = 'Not found'; - protected static $defaultHttpCode = 404; + protected static $defaultKey = 'notFound'; + protected static $defaultFallback = 'Not found'; + protected static $defaultHttpCode = 404; } diff --git a/kirby/src/Exception/PermissionException.php b/kirby/src/Exception/PermissionException.php index 8cf2a33..4863f66 100644 --- a/kirby/src/Exception/PermissionException.php +++ b/kirby/src/Exception/PermissionException.php @@ -15,7 +15,7 @@ namespace Kirby\Exception; */ class PermissionException extends Exception { - protected static $defaultKey = 'permission'; - protected static $defaultFallback = 'You are not allowed to do this'; - protected static $defaultHttpCode = 403; + protected static $defaultKey = 'permission'; + protected static $defaultFallback = 'You are not allowed to do this'; + protected static $defaultHttpCode = 403; } diff --git a/kirby/src/Filesystem/Asset.php b/kirby/src/Filesystem/Asset.php index 74a0124..cd6488b 100644 --- a/kirby/src/Filesystem/Asset.php +++ b/kirby/src/Filesystem/Asset.php @@ -18,100 +18,100 @@ use Kirby\Cms\FileModifications; */ class Asset { - use IsFile; - use FileModifications; + use IsFile; + use FileModifications; - /** - * Relative file path - * - * @var string - */ - protected $path; + /** + * Relative file path + * + * @var string + */ + protected $path; - /** - * Creates a new Asset object for the given path. - * - * @param string $path - */ - public function __construct(string $path) - { - $this->setProperties([ - 'path' => dirname($path), - 'root' => $this->kirby()->root('index') . '/' . $path, - 'url' => $this->kirby()->url('index') . '/' . $path - ]); - } + /** + * Creates a new Asset object for the given path. + * + * @param string $path + */ + public function __construct(string $path) + { + $this->setProperties([ + 'path' => dirname($path), + 'root' => $this->kirby()->root('index') . '/' . $path, + 'url' => $this->kirby()->url('base') . '/' . $path + ]); + } - /** - * Returns a unique id for the asset - * - * @return string - */ - public function id(): string - { - return $this->root(); - } + /** + * Returns a unique id for the asset + * + * @return string + */ + public function id(): string + { + return $this->root(); + } - /** - * Create a unique media hash - * - * @return string - */ - public function mediaHash(): string - { - return crc32($this->filename()) . '-' . $this->modified(); - } + /** + * Create a unique media hash + * + * @return string + */ + public function mediaHash(): string + { + return crc32($this->filename()) . '-' . $this->modified(); + } - /** - * Returns the relative path starting at the media folder - * - * @return string - */ - public function mediaPath(): string - { - return 'assets/' . $this->path() . '/' . $this->mediaHash() . '/' . $this->filename(); - } + /** + * Returns the relative path starting at the media folder + * + * @return string + */ + public function mediaPath(): string + { + return 'assets/' . $this->path() . '/' . $this->mediaHash() . '/' . $this->filename(); + } - /** - * Returns the absolute path to the file in the public media folder - * - * @return string - */ - public function mediaRoot(): string - { - return $this->kirby()->root('media') . '/' . $this->mediaPath(); - } + /** + * Returns the absolute path to the file in the public media folder + * + * @return string + */ + public function mediaRoot(): string + { + return $this->kirby()->root('media') . '/' . $this->mediaPath(); + } - /** - * Returns the absolute Url to the file in the public media folder - * - * @return string - */ - public function mediaUrl(): string - { - return $this->kirby()->url('media') . '/' . $this->mediaPath(); - } + /** + * Returns the absolute Url to the file in the public media folder + * + * @return string + */ + public function mediaUrl(): string + { + return $this->kirby()->url('media') . '/' . $this->mediaPath(); + } - /** - * Returns the path of the file from the web root, - * excluding the filename - * - * @return string - */ - public function path(): string - { - return $this->path; - } + /** + * Returns the path of the file from the web root, + * excluding the filename + * + * @return string + */ + public function path(): string + { + return $this->path; + } - /** - * Setter for the path - * - * @param string $path - * @return $this - */ - protected function setPath(string $path) - { - $this->path = $path === '.' ? '' : $path; - return $this; - } + /** + * Setter for the path + * + * @param string $path + * @return $this + */ + protected function setPath(string $path) + { + $this->path = $path === '.' ? '' : $path; + return $this; + } } diff --git a/kirby/src/Filesystem/Dir.php b/kirby/src/Filesystem/Dir.php index 3a3c5ba..8207efc 100644 --- a/kirby/src/Filesystem/Dir.php +++ b/kirby/src/Filesystem/Dir.php @@ -28,591 +28,585 @@ use Throwable; */ class Dir { - /** - * Ignore when scanning directories - * - * @var array - */ - public static $ignore = [ - '.', - '..', - '.DS_Store', - '.gitignore', - '.git', - '.svn', - '.htaccess', - 'Thumb.db', - '@eaDir' - ]; - - public static $numSeparator = '_'; - - /** - * Copy the directory to a new destination - * - * @param string $dir - * @param string $target - * @param bool $recursive - * @param array $ignore - * @return bool - */ - public static function copy(string $dir, string $target, bool $recursive = true, array $ignore = []): bool - { - if (is_dir($dir) === false) { - throw new Exception('The directory "' . $dir . '" does not exist'); - } - - if (is_dir($target) === true) { - throw new Exception('The target directory "' . $target . '" exists'); - } - - if (static::make($target) !== true) { - throw new Exception('The target directory "' . $target . '" could not be created'); - } - - foreach (static::read($dir) as $name) { - $root = $dir . '/' . $name; - - if (in_array($root, $ignore) === true) { - continue; - } - - if (is_dir($root) === true) { - if ($recursive === true) { - static::copy($root, $target . '/' . $name, true, $ignore); - } - } else { - F::copy($root, $target . '/' . $name); - } - } - - return true; - } - - /** - * Get all subdirectories - * - * @param string $dir - * @param array $ignore - * @param bool $absolute - * @return array - */ - public static function dirs(string $dir, array $ignore = null, bool $absolute = false): array - { - $result = array_values(array_filter(static::read($dir, $ignore, true), 'is_dir')); - - if ($absolute !== true) { - $result = array_map('basename', $result); - } - - return $result; - } - - /** - * Checks if the directory exists on disk - * - * @param string $dir - * @return bool - */ - public static function exists(string $dir): bool - { - return is_dir($dir) === true; - } - - /** - * Get all files - * - * @param string $dir - * @param array $ignore - * @param bool $absolute - * @return array - */ - public static function files(string $dir, array $ignore = null, bool $absolute = false): array - { - $result = array_values(array_filter(static::read($dir, $ignore, true), 'is_file')); - - if ($absolute !== true) { - $result = array_map('basename', $result); - } - - return $result; - } - - /** - * Read the directory and all subdirectories - * - * @param string $dir - * @param bool $recursive - * @param array $ignore - * @param string $path - * @return array - */ - public static function index(string $dir, bool $recursive = false, array $ignore = null, string $path = null) - { - $result = []; - $dir = realpath($dir); - $items = static::read($dir); - - foreach ($items as $item) { - $root = $dir . '/' . $item; - $entry = $path !== null ? $path . '/' . $item : $item; - $result[] = $entry; - - if ($recursive === true && is_dir($root) === true) { - $result = array_merge($result, static::index($root, true, $ignore, $entry)); - } - } - - return $result; - } - - /** - * Checks if the folder has any contents - * - * @param string $dir - * @return bool - */ - public static function isEmpty(string $dir): bool - { - return count(static::read($dir)) === 0; - } - - /** - * Checks if the directory is readable - * - * @param string $dir - * @return bool - */ - public static function isReadable(string $dir): bool - { - return is_readable($dir); - } - - /** - * Checks if the directory is writable - * - * @param string $dir - * @return bool - */ - public static function isWritable(string $dir): bool - { - return is_writable($dir); - } - - /** - * Scans the directory and analyzes files, - * content, meta info and children. This is used - * in `Kirby\Cms\Page`, `Kirby\Cms\Site` and - * `Kirby\Cms\User` objects to fetch all - * relevant information. - * - * Don't use outside the Cms context. - * - * @internal - * - * @param string $dir - * @param string $contentExtension - * @param array|null $contentIgnore - * @param bool $multilang - * @return array - */ - public static function inventory(string $dir, string $contentExtension = 'txt', array $contentIgnore = null, bool $multilang = false): array - { - $dir = realpath($dir); - - $inventory = [ - 'children' => [], - 'files' => [], - 'template' => 'default', - ]; - - if ($dir === false) { - return $inventory; - } - - $items = static::read($dir, $contentIgnore); - - // a temporary store for all content files - $content = []; - - // sort all items naturally to avoid sorting issues later - natsort($items); - - foreach ($items as $item) { - - // ignore all items with a leading dot - if (in_array(substr($item, 0, 1), ['.', '_']) === true) { - continue; - } - - $root = $dir . '/' . $item; - - if (is_dir($root) === true) { - - // extract the slug and num of the directory - if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) { - $num = (int)$match[1]; - $slug = $match[2]; - } else { - $num = null; - $slug = $item; - } - - $inventory['children'][] = [ - 'dirname' => $item, - 'model' => null, - 'num' => $num, - 'root' => $root, - 'slug' => $slug, - ]; - } else { - $extension = pathinfo($item, PATHINFO_EXTENSION); - - switch ($extension) { - case 'htm': - case 'html': - case 'php': - // don't track those files - break; - case $contentExtension: - $content[] = pathinfo($item, PATHINFO_FILENAME); - break; - default: - $inventory['files'][$item] = [ - 'filename' => $item, - 'extension' => $extension, - 'root' => $root, - ]; - } - } - } - - // remove the language codes from all content filenames - if ($multilang === true) { - foreach ($content as $key => $filename) { - $content[$key] = pathinfo($filename, PATHINFO_FILENAME); - } - - $content = array_unique($content); - } - - $inventory = static::inventoryContent($inventory, $content); - $inventory = static::inventoryModels($inventory, $contentExtension, $multilang); - - return $inventory; - } - - /** - * Take all content files, - * remove those who are meta files and - * detect the main content file - * - * @param array $inventory - * @param array $content - * @return array - */ - protected static function inventoryContent(array $inventory, array $content): array - { - - // filter meta files from the content file - if (empty($content) === true) { - $inventory['template'] = 'default'; - return $inventory; - } - - foreach ($content as $contentName) { - - // could be a meta file. i.e. cover.jpg - if (isset($inventory['files'][$contentName]) === true) { - continue; - } - - // it's most likely the template - $inventory['template'] = $contentName; - } - - return $inventory; - } - - /** - * Go through all inventory children - * and inject a model for each - * - * @param array $inventory - * @param string $contentExtension - * @param bool $multilang - * @return array - */ - protected static function inventoryModels(array $inventory, string $contentExtension, bool $multilang = false): array - { - // inject models - if (empty($inventory['children']) === false && empty(Page::$models) === false) { - if ($multilang === true) { - $contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension; - } - - foreach ($inventory['children'] as $key => $child) { - foreach (Page::$models as $modelName => $modelClass) { - if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) { - $inventory['children'][$key]['model'] = $modelName; - break; - } - } - } - } - - return $inventory; - } - - /** - * Create a (symbolic) link to a directory - * - * @param string $source - * @param string $link - * @return bool - */ - public static function link(string $source, string $link): bool - { - static::make(dirname($link), true); - - if (is_dir($link) === true) { - return true; - } - - if (is_dir($source) === false) { - throw new Exception(sprintf('The directory "%s" does not exist and cannot be linked', $source)); - } - - try { - return symlink($source, $link) === true; - } catch (Throwable $e) { - return false; - } - } - - /** - * Creates a new directory - * - * @param string $dir The path for the new directory - * @param bool $recursive Create all parent directories, which don't exist - * @return bool True: the dir has been created, false: creating failed - * @throws \Exception If a file with the provided path already exists or the parent directory is not writable - */ - public static function make(string $dir, bool $recursive = true): bool - { - if (empty($dir) === true) { - return false; - } - - if (is_dir($dir) === true) { - return true; - } - - if (is_file($dir) === true) { - throw new Exception(sprintf('A file with the name "%s" already exists', $dir)); - } - - $parent = dirname($dir); - - if ($recursive === true) { - if (is_dir($parent) === false) { - static::make($parent, true); - } - } - - if (is_writable($parent) === false) { - throw new Exception(sprintf('The directory "%s" cannot be created', $dir)); - } - - return mkdir($dir); - } - - /** - * Recursively check when the dir and all - * subfolders have been modified for the last time. - * - * @param string $dir The path of the directory - * @param string $format - * @param string $handler - * @return int|string - */ - public static function modified(string $dir, string $format = null, string $handler = 'date') - { - $modified = filemtime($dir); - $items = static::read($dir); - - foreach ($items as $item) { - if (is_file($dir . '/' . $item) === true) { - $newModified = filemtime($dir . '/' . $item); - } else { - $newModified = static::modified($dir . '/' . $item); - } - - $modified = ($newModified > $modified) ? $newModified : $modified; - } - - return Str::date($modified, $format, $handler); - } - - /** - * Moves a directory to a new location - * - * @param string $old The current path of the directory - * @param string $new The desired path where the dir should be moved to - * @return bool true: the directory has been moved, false: moving failed - */ - public static function move(string $old, string $new): bool - { - if ($old === $new) { - return true; - } - - if (is_dir($old) === false || is_dir($new) === true) { - return false; - } - - if (static::make(dirname($new), true) !== true) { - throw new Exception('The parent directory cannot be created'); - } - - return rename($old, $new); - } - - /** - * Returns a nicely formatted size of all the contents of the folder - * - * @param string $dir The path of the directory - * @param string|null|false $locale Locale for number formatting, - * `null` for the current locale, - * `false` to disable number formatting - * @return mixed - */ - public static function niceSize(string $dir, $locale = null) - { - return F::niceSize(static::size($dir), $locale); - } - - /** - * Reads all files from a directory and returns them as an array. - * It skips unwanted invisible stuff. - * - * @param string $dir The path of directory - * @param array $ignore Optional array with filenames, which should be ignored - * @param bool $absolute If true, the full path for each item will be returned - * @return array An array of filenames - */ - public static function read(string $dir, array $ignore = null, bool $absolute = false): array - { - if (is_dir($dir) === false) { - return []; - } - - // create the ignore pattern - $ignore ??= static::$ignore; - $ignore = array_merge($ignore, ['.', '..']); - - // scan for all files and dirs - $result = array_values((array)array_diff(scandir($dir), $ignore)); - - // add absolute paths - if ($absolute === true) { - $result = array_map(fn ($item) => $dir . '/' . $item, $result); - } - - return $result; - } - - /** - * Removes a folder including all containing files and folders - * - * @param string $dir - * @return bool - */ - public static function remove(string $dir): bool - { - $dir = realpath($dir); - - if (is_dir($dir) === false) { - return true; - } - - if (is_link($dir) === true) { - return unlink($dir); - } - - foreach (scandir($dir) as $childName) { - if (in_array($childName, ['.', '..']) === true) { - continue; - } - - $child = $dir . '/' . $childName; - - if (is_link($child) === true) { - unlink($child); - } elseif (is_dir($child) === true) { - static::remove($child); - } else { - F::remove($child); - } - } - - return rmdir($dir); - } - - /** - * Gets the size of the directory - * - * @param string $dir The path of the directory - * @param bool $recursive Include all subfolders and their files - * @return mixed - */ - public static function size(string $dir, bool $recursive = true) - { - if (is_dir($dir) === false) { - return false; - } - - // Get size for all direct files - $size = F::size(static::files($dir, null, true)); - - // if recursive, add sizes of all subdirectories - if ($recursive === true) { - foreach (static::dirs($dir, null, true) as $subdir) { - $size += static::size($subdir); - } - } - - return $size; - } - - /** - * Checks if the directory or any subdirectory has been - * modified after the given timestamp - * - * @param string $dir - * @param int $time - * @return bool - */ - public static function wasModifiedAfter(string $dir, int $time): bool - { - if (filemtime($dir) > $time) { - return true; - } - - $content = static::read($dir); - - foreach ($content as $item) { - $subdir = $dir . '/' . $item; - - if (filemtime($subdir) > $time) { - return true; - } - - if (is_dir($subdir) === true && static::wasModifiedAfter($subdir, $time) === true) { - return true; - } - } - - return false; - } + /** + * Ignore when scanning directories + * + * @var array + */ + public static $ignore = [ + '.', + '..', + '.DS_Store', + '.gitignore', + '.git', + '.svn', + '.htaccess', + 'Thumb.db', + '@eaDir' + ]; + + public static $numSeparator = '_'; + + /** + * Copy the directory to a new destination + * + * @param string $dir + * @param string $target + * @param bool $recursive + * @param array $ignore + * @return bool + */ + public static function copy(string $dir, string $target, bool $recursive = true, array $ignore = []): bool + { + if (is_dir($dir) === false) { + throw new Exception('The directory "' . $dir . '" does not exist'); + } + + if (is_dir($target) === true) { + throw new Exception('The target directory "' . $target . '" exists'); + } + + if (static::make($target) !== true) { + throw new Exception('The target directory "' . $target . '" could not be created'); + } + + foreach (static::read($dir) as $name) { + $root = $dir . '/' . $name; + + if (in_array($root, $ignore) === true) { + continue; + } + + if (is_dir($root) === true) { + if ($recursive === true) { + static::copy($root, $target . '/' . $name, true, $ignore); + } + } else { + F::copy($root, $target . '/' . $name); + } + } + + return true; + } + + /** + * Get all subdirectories + * + * @param string $dir + * @param array $ignore + * @param bool $absolute + * @return array + */ + public static function dirs(string $dir, array $ignore = null, bool $absolute = false): array + { + $result = array_values(array_filter(static::read($dir, $ignore, true), 'is_dir')); + + if ($absolute !== true) { + $result = array_map('basename', $result); + } + + return $result; + } + + /** + * Checks if the directory exists on disk + * + * @param string $dir + * @return bool + */ + public static function exists(string $dir): bool + { + return is_dir($dir) === true; + } + + /** + * Get all files + * + * @param string $dir + * @param array $ignore + * @param bool $absolute + * @return array + */ + public static function files(string $dir, array $ignore = null, bool $absolute = false): array + { + $result = array_values(array_filter(static::read($dir, $ignore, true), 'is_file')); + + if ($absolute !== true) { + $result = array_map('basename', $result); + } + + return $result; + } + + /** + * Read the directory and all subdirectories + * + * @param string $dir + * @param bool $recursive + * @param array $ignore + * @param string $path + * @return array + */ + public static function index(string $dir, bool $recursive = false, array $ignore = null, string $path = null) + { + $result = []; + $dir = realpath($dir); + $items = static::read($dir); + + foreach ($items as $item) { + $root = $dir . '/' . $item; + $entry = $path !== null ? $path . '/' . $item : $item; + $result[] = $entry; + + if ($recursive === true && is_dir($root) === true) { + $result = array_merge($result, static::index($root, true, $ignore, $entry)); + } + } + + return $result; + } + + /** + * Checks if the folder has any contents + * + * @param string $dir + * @return bool + */ + public static function isEmpty(string $dir): bool + { + return count(static::read($dir)) === 0; + } + + /** + * Checks if the directory is readable + * + * @param string $dir + * @return bool + */ + public static function isReadable(string $dir): bool + { + return is_readable($dir); + } + + /** + * Checks if the directory is writable + * + * @param string $dir + * @return bool + */ + public static function isWritable(string $dir): bool + { + return is_writable($dir); + } + + /** + * Scans the directory and analyzes files, + * content, meta info and children. This is used + * in `Kirby\Cms\Page`, `Kirby\Cms\Site` and + * `Kirby\Cms\User` objects to fetch all + * relevant information. + * + * Don't use outside the Cms context. + * + * @internal + * + * @param string $dir + * @param string $contentExtension + * @param array|null $contentIgnore + * @param bool $multilang + * @return array + */ + public static function inventory(string $dir, string $contentExtension = 'txt', array $contentIgnore = null, bool $multilang = false): array + { + $dir = realpath($dir); + + $inventory = [ + 'children' => [], + 'files' => [], + 'template' => 'default', + ]; + + if ($dir === false) { + return $inventory; + } + + $items = static::read($dir, $contentIgnore); + + // a temporary store for all content files + $content = []; + + // sort all items naturally to avoid sorting issues later + natsort($items); + + foreach ($items as $item) { + // ignore all items with a leading dot + if (in_array(substr($item, 0, 1), ['.', '_']) === true) { + continue; + } + + $root = $dir . '/' . $item; + + if (is_dir($root) === true) { + // extract the slug and num of the directory + if (preg_match('/^([0-9]+)' . static::$numSeparator . '(.*)$/', $item, $match)) { + $num = (int)$match[1]; + $slug = $match[2]; + } else { + $num = null; + $slug = $item; + } + + $inventory['children'][] = [ + 'dirname' => $item, + 'model' => null, + 'num' => $num, + 'root' => $root, + 'slug' => $slug, + ]; + } else { + $extension = pathinfo($item, PATHINFO_EXTENSION); + + switch ($extension) { + case 'htm': + case 'html': + case 'php': + // don't track those files + break; + case $contentExtension: + $content[] = pathinfo($item, PATHINFO_FILENAME); + break; + default: + $inventory['files'][$item] = [ + 'filename' => $item, + 'extension' => $extension, + 'root' => $root, + ]; + } + } + } + + // remove the language codes from all content filenames + if ($multilang === true) { + foreach ($content as $key => $filename) { + $content[$key] = pathinfo($filename, PATHINFO_FILENAME); + } + + $content = array_unique($content); + } + + $inventory = static::inventoryContent($inventory, $content); + $inventory = static::inventoryModels($inventory, $contentExtension, $multilang); + + return $inventory; + } + + /** + * Take all content files, + * remove those who are meta files and + * detect the main content file + * + * @param array $inventory + * @param array $content + * @return array + */ + protected static function inventoryContent(array $inventory, array $content): array + { + // filter meta files from the content file + if (empty($content) === true) { + $inventory['template'] = 'default'; + return $inventory; + } + + foreach ($content as $contentName) { + // could be a meta file. i.e. cover.jpg + if (isset($inventory['files'][$contentName]) === true) { + continue; + } + + // it's most likely the template + $inventory['template'] = $contentName; + } + + return $inventory; + } + + /** + * Go through all inventory children + * and inject a model for each + * + * @param array $inventory + * @param string $contentExtension + * @param bool $multilang + * @return array + */ + protected static function inventoryModels(array $inventory, string $contentExtension, bool $multilang = false): array + { + // inject models + if (empty($inventory['children']) === false && empty(Page::$models) === false) { + if ($multilang === true) { + $contentExtension = App::instance()->defaultLanguage()->code() . '.' . $contentExtension; + } + + foreach ($inventory['children'] as $key => $child) { + foreach (Page::$models as $modelName => $modelClass) { + if (file_exists($child['root'] . '/' . $modelName . '.' . $contentExtension) === true) { + $inventory['children'][$key]['model'] = $modelName; + break; + } + } + } + } + + return $inventory; + } + + /** + * Create a (symbolic) link to a directory + * + * @param string $source + * @param string $link + * @return bool + */ + public static function link(string $source, string $link): bool + { + static::make(dirname($link), true); + + if (is_dir($link) === true) { + return true; + } + + if (is_dir($source) === false) { + throw new Exception(sprintf('The directory "%s" does not exist and cannot be linked', $source)); + } + + try { + return symlink($source, $link) === true; + } catch (Throwable $e) { + return false; + } + } + + /** + * Creates a new directory + * + * @param string $dir The path for the new directory + * @param bool $recursive Create all parent directories, which don't exist + * @return bool True: the dir has been created, false: creating failed + * @throws \Exception If a file with the provided path already exists or the parent directory is not writable + */ + public static function make(string $dir, bool $recursive = true): bool + { + if (empty($dir) === true) { + return false; + } + + if (is_dir($dir) === true) { + return true; + } + + if (is_file($dir) === true) { + throw new Exception(sprintf('A file with the name "%s" already exists', $dir)); + } + + $parent = dirname($dir); + + if ($recursive === true) { + if (is_dir($parent) === false) { + static::make($parent, true); + } + } + + if (is_writable($parent) === false) { + throw new Exception(sprintf('The directory "%s" cannot be created', $dir)); + } + + return mkdir($dir); + } + + /** + * Recursively check when the dir and all + * subfolders have been modified for the last time. + * + * @param string $dir The path of the directory + * @param string $format + * @param string $handler + * @return int|string + */ + public static function modified(string $dir, string $format = null, string $handler = 'date') + { + $modified = filemtime($dir); + $items = static::read($dir); + + foreach ($items as $item) { + if (is_file($dir . '/' . $item) === true) { + $newModified = filemtime($dir . '/' . $item); + } else { + $newModified = static::modified($dir . '/' . $item); + } + + $modified = ($newModified > $modified) ? $newModified : $modified; + } + + return Str::date($modified, $format, $handler); + } + + /** + * Moves a directory to a new location + * + * @param string $old The current path of the directory + * @param string $new The desired path where the dir should be moved to + * @return bool true: the directory has been moved, false: moving failed + */ + public static function move(string $old, string $new): bool + { + if ($old === $new) { + return true; + } + + if (is_dir($old) === false || is_dir($new) === true) { + return false; + } + + if (static::make(dirname($new), true) !== true) { + throw new Exception('The parent directory cannot be created'); + } + + return rename($old, $new); + } + + /** + * Returns a nicely formatted size of all the contents of the folder + * + * @param string $dir The path of the directory + * @param string|null|false $locale Locale for number formatting, + * `null` for the current locale, + * `false` to disable number formatting + * @return mixed + */ + public static function niceSize(string $dir, $locale = null) + { + return F::niceSize(static::size($dir), $locale); + } + + /** + * Reads all files from a directory and returns them as an array. + * It skips unwanted invisible stuff. + * + * @param string $dir The path of directory + * @param array $ignore Optional array with filenames, which should be ignored + * @param bool $absolute If true, the full path for each item will be returned + * @return array An array of filenames + */ + public static function read(string $dir, array $ignore = null, bool $absolute = false): array + { + if (is_dir($dir) === false) { + return []; + } + + // create the ignore pattern + $ignore ??= static::$ignore; + $ignore = array_merge($ignore, ['.', '..']); + + // scan for all files and dirs + $result = array_values((array)array_diff(scandir($dir), $ignore)); + + // add absolute paths + if ($absolute === true) { + $result = array_map(fn ($item) => $dir . '/' . $item, $result); + } + + return $result; + } + + /** + * Removes a folder including all containing files and folders + * + * @param string $dir + * @return bool + */ + public static function remove(string $dir): bool + { + $dir = realpath($dir); + + if (is_dir($dir) === false) { + return true; + } + + if (is_link($dir) === true) { + return F::unlink($dir); + } + + foreach (scandir($dir) as $childName) { + if (in_array($childName, ['.', '..']) === true) { + continue; + } + + $child = $dir . '/' . $childName; + + if (is_dir($child) === true && is_link($child) === false) { + static::remove($child); + } else { + F::unlink($child); + } + } + + return rmdir($dir); + } + + /** + * Gets the size of the directory + * + * @param string $dir The path of the directory + * @param bool $recursive Include all subfolders and their files + * @return mixed + */ + public static function size(string $dir, bool $recursive = true) + { + if (is_dir($dir) === false) { + return false; + } + + // Get size for all direct files + $size = F::size(static::files($dir, null, true)); + + // if recursive, add sizes of all subdirectories + if ($recursive === true) { + foreach (static::dirs($dir, null, true) as $subdir) { + $size += static::size($subdir); + } + } + + return $size; + } + + /** + * Checks if the directory or any subdirectory has been + * modified after the given timestamp + * + * @param string $dir + * @param int $time + * @return bool + */ + public static function wasModifiedAfter(string $dir, int $time): bool + { + if (filemtime($dir) > $time) { + return true; + } + + $content = static::read($dir); + + foreach ($content as $item) { + $subdir = $dir . '/' . $item; + + if (filemtime($subdir) > $time) { + return true; + } + + if (is_dir($subdir) === true && static::wasModifiedAfter($subdir, $time) === true) { + return true; + } + } + + return false; + } } diff --git a/kirby/src/Filesystem/F.php b/kirby/src/Filesystem/F.php index 375a219..5481242 100644 --- a/kirby/src/Filesystem/F.php +++ b/kirby/src/Filesystem/F.php @@ -3,6 +3,7 @@ namespace Kirby\Filesystem; use Exception; +use Kirby\Cms\Helpers; use Kirby\Toolkit\I18n; use Kirby\Toolkit\Str; use Throwable; @@ -22,868 +23,919 @@ use ZipArchive; */ class F { - /** - * @var array - */ - public static $types = [ - 'archive' => [ - 'gz', - 'gzip', - 'tar', - 'tgz', - 'zip', - ], - 'audio' => [ - 'aif', - 'aiff', - 'm4a', - 'midi', - 'mp3', - 'wav', - ], - 'code' => [ - 'css', - 'js', - 'json', - 'java', - 'htm', - 'html', - 'php', - 'rb', - 'py', - 'scss', - 'xml', - 'yaml', - 'yml', - ], - 'document' => [ - 'csv', - 'doc', - 'docx', - 'dotx', - 'indd', - 'md', - 'mdown', - 'pdf', - 'ppt', - 'pptx', - 'rtf', - 'txt', - 'xl', - 'xls', - 'xlsx', - 'xltx', - ], - 'image' => [ - 'ai', - 'avif', - 'bmp', - 'gif', - 'eps', - 'ico', - 'j2k', - 'jp2', - 'jpeg', - 'jpg', - 'jpe', - 'png', - 'ps', - 'psd', - 'svg', - 'tif', - 'tiff', - 'webp' - ], - 'video' => [ - 'avi', - 'flv', - 'm4v', - 'mov', - 'movie', - 'mpe', - 'mpg', - 'mp4', - 'ogg', - 'ogv', - 'swf', - 'webm', - ], - ]; - - /** - * @var array - */ - public static $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - - /** - * Appends new content to an existing file - * - * @param string $file The path for the file - * @param mixed $content Either a string or an array. Arrays will be converted to JSON. - * @return bool - */ - public static function append(string $file, $content): bool - { - return static::write($file, $content, true); - } - - /** - * Returns the file content as base64 encoded string - * - * @param string $file The path for the file - * @return string - */ - public static function base64(string $file): string - { - return base64_encode(static::read($file)); - } - - /** - * Copy a file to a new location. - * - * @param string $source - * @param string $target - * @param bool $force - * @return bool - */ - public static function copy(string $source, string $target, bool $force = false): bool - { - if (file_exists($source) === false || (file_exists($target) === true && $force === false)) { - return false; - } - - $directory = dirname($target); - - // create the parent directory if it does not exist - if (is_dir($directory) === false) { - Dir::make($directory, true); - } - - return copy($source, $target); - } - - /** - * Just an alternative for dirname() to stay consistent - * - * - * - * $dirname = F::dirname('/var/www/test.txt'); - * // dirname is /var/www - * - * - * - * @param string $file The path - * @return string - */ - public static function dirname(string $file): string - { - return dirname($file); - } - - /** - * Checks if the file exists on disk - * - * @param string $file - * @param string $in - * @return bool - */ - public static function exists(string $file, string $in = null): bool - { - try { - static::realpath($file, $in); - return true; - } catch (Exception $e) { - return false; - } - } - - /** - * Gets the extension of a file - * - * @param string $file The filename or path - * @param string $extension Set an optional extension to overwrite the current one - * @return string - */ - public static function extension(string $file = null, string $extension = null): string - { - // overwrite the current extension - if ($extension !== null) { - return static::name($file) . '.' . $extension; - } - - // return the current extension - return Str::lower(pathinfo($file, PATHINFO_EXTENSION)); - } - - /** - * Converts a file extension to a mime type - * - * @param string $extension - * @return string|false - */ - public static function extensionToMime(string $extension) - { - return Mime::fromExtension($extension); - } - - /** - * Returns the file type for a passed extension - * - * @param string $extension - * @return string|false - */ - public static function extensionToType(string $extension) - { - foreach (static::$types as $type => $extensions) { - if (in_array($extension, $extensions) === true) { - return $type; - } - } - - return false; - } - - /** - * Returns all extensions for a certain file type - * - * @param string $type - * @return array - */ - public static function extensions(string $type = null) - { - if ($type === null) { - return array_keys(Mime::types()); - } - - return static::$types[$type] ?? []; - } - - /** - * Extracts the filename from a file path - * - * - * - * $filename = F::filename('/var/www/test.txt'); - * // filename is test.txt - * - * - * - * @param string $name The path - * @return string - */ - public static function filename(string $name): string - { - return pathinfo($name, PATHINFO_BASENAME); - } - - /** - * Invalidate opcode cache for file. - * - * @param string $file The path of the file - * @return bool - */ - public static function invalidateOpcodeCache(string $file): bool - { - if (function_exists('opcache_invalidate') && strlen(ini_get('opcache.restrict_api')) === 0) { - return opcache_invalidate($file, true); - } else { - return false; - } - } - - /** - * Checks if a file is of a certain type - * - * @param string $file Full path to the file - * @param string $value An extension or mime type - * @return bool - */ - public static function is(string $file, string $value): bool - { - // check for the extension - if (in_array($value, static::extensions()) === true) { - return static::extension($file) === $value; - } - - // check for the mime type - if (strpos($value, '/') !== false) { - return static::mime($file) === $value; - } - - return false; - } - - /** - * Checks if the file is readable - * - * @param string $file - * @return bool - */ - public static function isReadable(string $file): bool - { - return is_readable($file); - } - - /** - * Checks if the file is writable - * - * @param string $file - * @return bool - */ - public static function isWritable(string $file): bool - { - if (file_exists($file) === false) { - return is_writable(dirname($file)); - } - - return is_writable($file); - } - - /** - * Create a (symbolic) link to a file - * - * @param string $source - * @param string $link - * @param string $method - * @return bool - */ - public static function link(string $source, string $link, string $method = 'link'): bool - { - Dir::make(dirname($link), true); - - if (is_file($link) === true) { - return true; - } - - if (is_file($source) === false) { - throw new Exception(sprintf('The file "%s" does not exist and cannot be linked', $source)); - } - - try { - return $method($source, $link) === true; - } catch (Throwable $e) { - return false; - } - } - - /** - * Loads a file and returns the result or `false` if the - * file to load does not exist - * - * @param string $file - * @param mixed $fallback - * @param array $data Optional array of variables to extract in the variable scope - * @return mixed - */ - public static function load(string $file, $fallback = null, array $data = []) - { - if (is_file($file) === false) { - return $fallback; - } - - // we use the loadIsolated() method here to prevent the included - // file from overwriting our $fallback in this variable scope; see - // https://www.php.net/manual/en/function.include.php#example-124 - $result = static::loadIsolated($file, $data); - - if ($fallback !== null && gettype($result) !== gettype($fallback)) { - return $fallback; - } - - return $result; - } - - /** - * Loads a file with as little as possible in the variable scope - * - * @param string $file - * @param array $data Optional array of variables to extract in the variable scope - * @return mixed - */ - protected static function loadIsolated(string $file, array $data = []) - { - // extract the $data variables in this scope to be accessed by the included file; - // protect $file against being overwritten by a $data variable - $___file___ = $file; - extract($data); - - return include $___file___; - } - - /** - * Loads a file using `include_once()` and returns whether loading was successful - * - * @param string $file - * @return bool - */ - public static function loadOnce(string $file): bool - { - if (is_file($file) === false) { - return false; - } - - include_once $file; - return true; - } - - /** - * Returns the mime type of a file - * - * @param string $file - * @return string|false - */ - public static function mime(string $file) - { - return Mime::type($file); - } - - /** - * Converts a mime type to a file extension - * - * @param string $mime - * @return string|false - */ - public static function mimeToExtension(string $mime = null) - { - return Mime::toExtension($mime); - } - - /** - * Returns the type for a given mime - * - * @param string $mime - * @return string|false - */ - public static function mimeToType(string $mime) - { - return static::extensionToType(Mime::toExtension($mime)); - } - - /** - * Get the file's last modification time. - * - * @param string $file - * @param string $format - * @param string $handler date or strftime - * @return mixed - */ - public static function modified(string $file, string $format = null, string $handler = 'date') - { - if (file_exists($file) !== true) { - return false; - } - - $modified = filemtime($file); - - return Str::date($modified, $format, $handler); - } - - /** - * Moves a file to a new location - * - * @param string $oldRoot The current path for the file - * @param string $newRoot The path to the new location - * @param bool $force Force move if the target file exists - * @return bool - */ - public static function move(string $oldRoot, string $newRoot, bool $force = false): bool - { - // check if the file exists - if (file_exists($oldRoot) === false) { - return false; - } - - if (file_exists($newRoot) === true) { - if ($force === false) { - return false; - } - - // delete the existing file - static::remove($newRoot); - } - - // actually move the file if it exists - if (rename($oldRoot, $newRoot) !== true) { - return false; - } - - return true; - } - - /** - * Extracts the name from a file path or filename without extension - * - * @param string $name The path or filename - * @return string - */ - public static function name(string $name): string - { - return pathinfo($name, PATHINFO_FILENAME); - } - - /** - * Converts an integer size into a human readable format - * - * @param mixed $size The file size, a file path or array of paths - * @param string|null|false $locale Locale for number formatting, - * `null` for the current locale, - * `false` to disable number formatting - * @return string - */ - public static function niceSize($size, $locale = null): string - { - // file mode - if (is_string($size) === true || is_array($size) === true) { - $size = static::size($size); - } - - // make sure it's an int - $size = (int)$size; - - // avoid errors for invalid sizes - if ($size <= 0) { - return '0 KB'; - } - - // the math magic - $size = round($size / pow(1024, ($unit = floor(log($size, 1024)))), 2); - - // format the number if requested - if ($locale !== false) { - $size = I18n::formatNumber($size, $locale); - } - - return $size . ' ' . static::$units[$unit]; - } - - /** - * Reads the content of a file or requests the - * contents of a remote HTTP or HTTPS URL - * - * @param string $file The path for the file or an absolute URL - * @return string|false - */ - public static function read(string $file) - { - if ( - is_file($file) !== true && - Str::startsWith($file, 'https://') !== true && - Str::startsWith($file, 'http://') !== true - ) { - return false; - } - - return @file_get_contents($file); - } - - /** - * Changes the name of the file without - * touching the extension - * - * @param string $file - * @param string $newName - * @param bool $overwrite Force overwrite existing files - * @return string|false - */ - public static function rename(string $file, string $newName, bool $overwrite = false) - { - // create the new name - $name = static::safeName(basename($newName)); - - // overwrite the root - $newRoot = rtrim(dirname($file) . '/' . $name . '.' . F::extension($file), '.'); - - // nothing has changed - if ($newRoot === $file) { - return $newRoot; - } - - if (F::move($file, $newRoot, $overwrite) !== true) { - return false; - } - - return $newRoot; - } - - /** - * Returns the absolute path to the file if the file can be found. - * - * @param string $file - * @param string $in - * @return string|null - */ - public static function realpath(string $file, string $in = null) - { - $realpath = realpath($file); - - if ($realpath === false || is_file($realpath) === false) { - throw new Exception(sprintf('The file does not exist at the given path: "%s"', $file)); - } - - if ($in !== null) { - $parent = realpath($in); - - if ($parent === false || is_dir($parent) === false) { - throw new Exception(sprintf('The parent directory does not exist: "%s"', $in)); - } - - if (substr($realpath, 0, strlen($parent)) !== $parent) { - throw new Exception('The file is not within the parent directory'); - } - } - - return $realpath; - } - - /** - * Returns the relative path of the file - * starting after $in - * - * @SuppressWarnings(PHPMD.CountInLoopExpression) - * - * @param string $file - * @param string $in - * @return string - */ - public static function relativepath(string $file, string $in = null): string - { - if (empty($in) === true) { - return basename($file); - } - - // windows - $file = str_replace('\\', '/', $file); - $in = str_replace('\\', '/', $in); - - // trim trailing slashes - $file = rtrim($file, '/'); - $in = rtrim($in, '/'); - - if (Str::contains($file, $in . '/') === false) { - // make the paths relative by stripping what they have - // in common and adding `../` tokens at the start - $fileParts = explode('/', $file); - $inParts = explode('/', $in); - while (count($fileParts) && count($inParts) && ($fileParts[0] === $inParts[0])) { - array_shift($fileParts); - array_shift($inParts); - } - - return str_repeat('../', count($inParts)) . implode('/', $fileParts); - } - - return '/' . Str::after($file, $in . '/'); - } - - /** - * Deletes a file - * - * - * - * $remove = F::remove('test.txt'); - * if($remove) echo 'The file has been removed'; - * - * - * - * @param string $file The path for the file - * @return bool - */ - public static function remove(string $file): bool - { - if (strpos($file, '*') !== false) { - foreach (glob($file) as $f) { - static::remove($f); - } - - return true; - } - - $file = realpath($file); - - if (file_exists($file) === false) { - return true; - } - - return unlink($file); - } - - /** - * Sanitize a filename to strip unwanted special characters - * - * - * - * $safe = f::safeName('über genius.txt'); - * // safe will be ueber-genius.txt - * - * - * - * @param string $string The file name - * @return string - */ - public static function safeName(string $string): string - { - $name = static::name($string); - $extension = static::extension($string); - $safeName = Str::slug($name, '-', 'a-z0-9@._-'); - $safeExtension = empty($extension) === false ? '.' . Str::slug($extension) : ''; - - return $safeName . $safeExtension; - } - - /** - * Tries to find similar or the same file by - * building a glob based on the path - * - * @param string $path - * @param string $pattern - * @return array - */ - public static function similar(string $path, string $pattern = '*'): array - { - $dir = dirname($path); - $name = static::name($path); - $extension = static::extension($path); - $glob = $dir . '/' . $name . $pattern . '.' . $extension; - return glob($glob); - } - - /** - * Returns the size of a file or an array of files. - * - * @param string|array $file file path or array of paths - * @return int - */ - public static function size($file): int - { - if (is_array($file) === true) { - return array_reduce( - $file, - fn ($total, $file) => $total + F::size($file), - 0 - ); - } - - try { - return filesize($file); - } catch (Throwable $e) { - return 0; - } - } - - /** - * Categorize the file - * - * @param string $file Either the file path or extension - * @return string|null - */ - public static function type(string $file) - { - $length = strlen($file); - - if ($length >= 2 && $length <= 4) { - // use the file name as extension - $extension = $file; - } else { - // get the extension from the filename - $extension = pathinfo($file, PATHINFO_EXTENSION); - } - - if (empty($extension) === true) { - // detect the mime type first to get the most reliable extension - $mime = static::mime($file); - $extension = static::mimeToExtension($mime); - } - - // sanitize extension - $extension = strtolower($extension); - - foreach (static::$types as $type => $extensions) { - if (in_array($extension, $extensions) === true) { - return $type; - } - } - - return null; - } - - /** - * Returns all extensions of a given file type - * or `null` if the file type is unknown - * - * @param string $type - * @return array|null - */ - public static function typeToExtensions(string $type): ?array - { - return static::$types[$type] ?? null; - } - - /** - * Unzips a zip file - * - * @param string $file - * @param string $to - * @return bool - */ - public static function unzip(string $file, string $to): bool - { - if (class_exists('ZipArchive') === false) { - throw new Exception('The ZipArchive class is not available'); - } - - $zip = new ZipArchive(); - - if ($zip->open($file) === true) { - $zip->extractTo($to); - $zip->close(); - return true; - } - - return false; - } - - /** - * Returns the file as data uri - * - * @param string $file The path for the file - * @return string|false - */ - public static function uri(string $file) - { - if ($mime = static::mime($file)) { - return 'data:' . $mime . ';base64,' . static::base64($file); - } - - return false; - } - - /** - * Creates a new file - * - * @param string $file The path for the new file - * @param mixed $content Either a string, an object or an array. Arrays and objects will be serialized. - * @param bool $append true: append the content to an existing file if available. false: overwrite. - * @return bool - */ - public static function write(string $file, $content, bool $append = false): bool - { - if (is_array($content) === true || is_object($content) === true) { - $content = serialize($content); - } - - $mode = $append === true ? FILE_APPEND | LOCK_EX : LOCK_EX; - - // if the parent directory does not exist, create it - if (is_dir(dirname($file)) === false) { - if (Dir::make(dirname($file)) === false) { - return false; - } - } - - if (static::isWritable($file) === false) { - throw new Exception('The file "' . $file . '" is not writable'); - } - - return file_put_contents($file, $content, $mode) !== false; - } + /** + * @var array + */ + public static $types = [ + 'archive' => [ + 'gz', + 'gzip', + 'tar', + 'tgz', + 'zip', + ], + 'audio' => [ + 'aif', + 'aiff', + 'm4a', + 'midi', + 'mp3', + 'wav', + ], + 'code' => [ + 'css', + 'js', + 'json', + 'java', + 'htm', + 'html', + 'php', + 'rb', + 'py', + 'scss', + 'xml', + 'yaml', + 'yml', + ], + 'document' => [ + 'csv', + 'doc', + 'docx', + 'dotx', + 'indd', + 'md', + 'mdown', + 'pdf', + 'ppt', + 'pptx', + 'rtf', + 'txt', + 'xl', + 'xls', + 'xlsx', + 'xltx', + ], + 'image' => [ + 'ai', + 'avif', + 'bmp', + 'gif', + 'eps', + 'ico', + 'j2k', + 'jp2', + 'jpeg', + 'jpg', + 'jpe', + 'png', + 'ps', + 'psd', + 'svg', + 'tif', + 'tiff', + 'webp' + ], + 'video' => [ + 'avi', + 'flv', + 'm4v', + 'mov', + 'movie', + 'mpe', + 'mpg', + 'mp4', + 'ogg', + 'ogv', + 'swf', + 'webm', + ], + ]; + + /** + * @var array + */ + public static $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + /** + * Appends new content to an existing file + * + * @param string $file The path for the file + * @param mixed $content Either a string or an array. Arrays will be converted to JSON. + * @return bool + */ + public static function append(string $file, $content): bool + { + return static::write($file, $content, true); + } + + /** + * Returns the file content as base64 encoded string + * + * @param string $file The path for the file + * @return string + */ + public static function base64(string $file): string + { + return base64_encode(static::read($file)); + } + + /** + * Copy a file to a new location. + * + * @param string $source + * @param string $target + * @param bool $force + * @return bool + */ + public static function copy(string $source, string $target, bool $force = false): bool + { + if (file_exists($source) === false || (file_exists($target) === true && $force === false)) { + return false; + } + + $directory = dirname($target); + + // create the parent directory if it does not exist + if (is_dir($directory) === false) { + Dir::make($directory, true); + } + + return copy($source, $target); + } + + /** + * Just an alternative for dirname() to stay consistent + * + * + * + * $dirname = F::dirname('/var/www/test.txt'); + * // dirname is /var/www + * + * + * + * @param string $file The path + * @return string + */ + public static function dirname(string $file): string + { + return dirname($file); + } + + /** + * Checks if the file exists on disk + * + * @param string $file + * @param string $in + * @return bool + */ + public static function exists(string $file, string $in = null): bool + { + try { + static::realpath($file, $in); + return true; + } catch (Exception $e) { + return false; + } + } + + /** + * Gets the extension of a file + * + * @param string $file The filename or path + * @param string $extension Set an optional extension to overwrite the current one + * @return string + */ + public static function extension(string $file = null, string $extension = null): string + { + // overwrite the current extension + if ($extension !== null) { + return static::name($file) . '.' . $extension; + } + + // return the current extension + return Str::lower(pathinfo($file, PATHINFO_EXTENSION)); + } + + /** + * Converts a file extension to a mime type + * + * @param string $extension + * @return string|false + */ + public static function extensionToMime(string $extension) + { + return Mime::fromExtension($extension); + } + + /** + * Returns the file type for a passed extension + * + * @param string $extension + * @return string|false + */ + public static function extensionToType(string $extension) + { + foreach (static::$types as $type => $extensions) { + if (in_array($extension, $extensions) === true) { + return $type; + } + } + + return false; + } + + /** + * Returns all extensions for a certain file type + * + * @param string $type + * @return array + */ + public static function extensions(string $type = null) + { + if ($type === null) { + return array_keys(Mime::types()); + } + + return static::$types[$type] ?? []; + } + + /** + * Extracts the filename from a file path + * + * + * + * $filename = F::filename('/var/www/test.txt'); + * // filename is test.txt + * + * + * + * @param string $name The path + * @return string + */ + public static function filename(string $name): string + { + return pathinfo($name, PATHINFO_BASENAME); + } + + /** + * Invalidate opcode cache for file. + * + * @param string $file The path of the file + * @return bool + */ + public static function invalidateOpcodeCache(string $file): bool + { + if (function_exists('opcache_invalidate') && strlen(ini_get('opcache.restrict_api')) === 0) { + return opcache_invalidate($file, true); + } else { + return false; + } + } + + /** + * Checks if a file is of a certain type + * + * @param string $file Full path to the file + * @param string $value An extension or mime type + * @return bool + */ + public static function is(string $file, string $value): bool + { + // check for the extension + if (in_array($value, static::extensions()) === true) { + return static::extension($file) === $value; + } + + // check for the mime type + if (strpos($value, '/') !== false) { + return static::mime($file) === $value; + } + + return false; + } + + /** + * Checks if the file is readable + * + * @param string $file + * @return bool + */ + public static function isReadable(string $file): bool + { + return is_readable($file); + } + + /** + * Checks if the file is writable + * + * @param string $file + * @return bool + */ + public static function isWritable(string $file): bool + { + if (file_exists($file) === false) { + return is_writable(dirname($file)); + } + + return is_writable($file); + } + + /** + * Create a (symbolic) link to a file + * + * @param string $source + * @param string $link + * @param string $method + * @return bool + */ + public static function link(string $source, string $link, string $method = 'link'): bool + { + Dir::make(dirname($link), true); + + if (is_file($link) === true) { + return true; + } + + if (is_file($source) === false) { + throw new Exception(sprintf('The file "%s" does not exist and cannot be linked', $source)); + } + + try { + return $method($source, $link) === true; + } catch (Throwable $e) { + return false; + } + } + + /** + * Loads a file and returns the result or `false` if the + * file to load does not exist + * + * @param string $file + * @param mixed $fallback + * @param array $data Optional array of variables to extract in the variable scope + * @return mixed + */ + public static function load(string $file, $fallback = null, array $data = []) + { + if (is_file($file) === false) { + return $fallback; + } + + // we use the loadIsolated() method here to prevent the included + // file from overwriting our $fallback in this variable scope; see + // https://www.php.net/manual/en/function.include.php#example-124 + $result = static::loadIsolated($file, $data); + + if ($fallback !== null && gettype($result) !== gettype($fallback)) { + return $fallback; + } + + return $result; + } + + /** + * A super simple class autoloader + * @since 3.7.0 + * + * @param array $classmap + * @param string|null $base + * @return void + */ + public static function loadClasses(array $classmap, ?string $base = null): void + { + // convert all classnames to lowercase + $classmap = array_change_key_case($classmap); + + spl_autoload_register(function ($class) use ($classmap, $base) { + $class = strtolower($class); + + if (!isset($classmap[$class])) { + return false; + } + + if ($base) { + include $base . '/' . $classmap[$class]; + } else { + include $classmap[$class]; + } + }); + } + + /** + * Loads a file with as little as possible in the variable scope + * + * @param string $file + * @param array $data Optional array of variables to extract in the variable scope + * @return mixed + */ + protected static function loadIsolated(string $file, array $data = []) + { + // extract the $data variables in this scope to be accessed by the included file; + // protect $file against being overwritten by a $data variable + $___file___ = $file; + extract($data); + + return include $___file___; + } + + /** + * Loads a file using `include_once()` and returns whether loading was successful + * + * @param string $file + * @return bool + */ + public static function loadOnce(string $file): bool + { + if (is_file($file) === false) { + return false; + } + + include_once $file; + return true; + } + + /** + * Returns the mime type of a file + * + * @param string $file + * @return string|false + */ + public static function mime(string $file) + { + return Mime::type($file); + } + + /** + * Converts a mime type to a file extension + * + * @param string $mime + * @return string|false + */ + public static function mimeToExtension(string $mime = null) + { + return Mime::toExtension($mime); + } + + /** + * Returns the type for a given mime + * + * @param string $mime + * @return string|false + */ + public static function mimeToType(string $mime) + { + return static::extensionToType(Mime::toExtension($mime)); + } + + /** + * Get the file's last modification time. + * + * @param string $file + * @param string|\IntlDateFormatter|null $format + * @param string $handler date, intl or strftime + * @return mixed + */ + public static function modified(string $file, $format = null, string $handler = 'date') + { + if (file_exists($file) !== true) { + return false; + } + + $modified = filemtime($file); + + return Str::date($modified, $format, $handler); + } + + /** + * Moves a file to a new location + * + * @param string $oldRoot The current path for the file + * @param string $newRoot The path to the new location + * @param bool $force Force move if the target file exists + * @return bool + */ + public static function move(string $oldRoot, string $newRoot, bool $force = false): bool + { + // check if the file exists + if (file_exists($oldRoot) === false) { + return false; + } + + if (file_exists($newRoot) === true) { + if ($force === false) { + return false; + } + + // delete the existing file + static::remove($newRoot); + } + + // actually move the file if it exists + if (rename($oldRoot, $newRoot) !== true) { + return false; + } + + return true; + } + + /** + * Extracts the name from a file path or filename without extension + * + * @param string $name The path or filename + * @return string + */ + public static function name(string $name): string + { + return pathinfo($name, PATHINFO_FILENAME); + } + + /** + * Converts an integer size into a human readable format + * + * @param mixed $size The file size, a file path or array of paths + * @param string|null|false $locale Locale for number formatting, + * `null` for the current locale, + * `false` to disable number formatting + * @return string + */ + public static function niceSize($size, $locale = null): string + { + // file mode + if (is_string($size) === true || is_array($size) === true) { + $size = static::size($size); + } + + // make sure it's an int + $size = (int)$size; + + // avoid errors for invalid sizes + if ($size <= 0) { + return '0 KB'; + } + + // the math magic + $size = round($size / pow(1024, ($unit = floor(log($size, 1024)))), 2); + + // format the number if requested + if ($locale !== false) { + $size = I18n::formatNumber($size, $locale); + } + + return $size . ' ' . static::$units[$unit]; + } + + /** + * Reads the content of a file or requests the + * contents of a remote HTTP or HTTPS URL + * + * @param string $file The path for the file or an absolute URL + * @return string|false + */ + public static function read(string $file) + { + if ( + is_file($file) !== true && + Str::startsWith($file, 'https://') !== true && + Str::startsWith($file, 'http://') !== true + ) { + return false; + } + + return @file_get_contents($file); + } + + /** + * Changes the name of the file without + * touching the extension + * + * @param string $file + * @param string $newName + * @param bool $overwrite Force overwrite existing files + * @return string|false + */ + public static function rename(string $file, string $newName, bool $overwrite = false) + { + // create the new name + $name = static::safeName(basename($newName)); + + // overwrite the root + $newRoot = rtrim(dirname($file) . '/' . $name . '.' . F::extension($file), '.'); + + // nothing has changed + if ($newRoot === $file) { + return $newRoot; + } + + if (F::move($file, $newRoot, $overwrite) !== true) { + return false; + } + + return $newRoot; + } + + /** + * Returns the absolute path to the file if the file can be found. + * + * @param string $file + * @param string $in + * @return string|null + */ + public static function realpath(string $file, string $in = null) + { + $realpath = realpath($file); + + if ($realpath === false || is_file($realpath) === false) { + throw new Exception(sprintf('The file does not exist at the given path: "%s"', $file)); + } + + if ($in !== null) { + $parent = realpath($in); + + if ($parent === false || is_dir($parent) === false) { + throw new Exception(sprintf('The parent directory does not exist: "%s"', $in)); + } + + if (substr($realpath, 0, strlen($parent)) !== $parent) { + throw new Exception('The file is not within the parent directory'); + } + } + + return $realpath; + } + + /** + * Returns the relative path of the file + * starting after $in + * + * @SuppressWarnings(PHPMD.CountInLoopExpression) + * + * @param string $file + * @param string $in + * @return string + */ + public static function relativepath(string $file, string $in = null): string + { + if (empty($in) === true) { + return basename($file); + } + + // windows + $file = str_replace('\\', '/', $file); + $in = str_replace('\\', '/', $in); + + // trim trailing slashes + $file = rtrim($file, '/'); + $in = rtrim($in, '/'); + + if (Str::contains($file, $in . '/') === false) { + // make the paths relative by stripping what they have + // in common and adding `../` tokens at the start + $fileParts = explode('/', $file); + $inParts = explode('/', $in); + while (count($fileParts) && count($inParts) && ($fileParts[0] === $inParts[0])) { + array_shift($fileParts); + array_shift($inParts); + } + + return str_repeat('../', count($inParts)) . implode('/', $fileParts); + } + + return '/' . Str::after($file, $in . '/'); + } + + /** + * Deletes a file + * + * + * + * $remove = F::remove('test.txt'); + * if($remove) echo 'The file has been removed'; + * + * + * + * @param string $file The path for the file + * @return bool + */ + public static function remove(string $file): bool + { + if (strpos($file, '*') !== false) { + foreach (glob($file) as $f) { + static::remove($f); + } + + return true; + } + + $file = realpath($file); + if (is_string($file) === false) { + return true; + } + + return static::unlink($file); + } + + /** + * Sanitize a filename to strip unwanted special characters + * + * + * + * $safe = f::safeName('über genius.txt'); + * // safe will be ueber-genius.txt + * + * + * + * @param string $string The file name + * @return string + */ + public static function safeName(string $string): string + { + $name = static::name($string); + $extension = static::extension($string); + $safeName = Str::slug($name, '-', 'a-z0-9@._-'); + $safeExtension = empty($extension) === false ? '.' . Str::slug($extension) : ''; + + return $safeName . $safeExtension; + } + + /** + * Tries to find similar or the same file by + * building a glob based on the path + * + * @param string $path + * @param string $pattern + * @return array + */ + public static function similar(string $path, string $pattern = '*'): array + { + $dir = dirname($path); + $name = static::name($path); + $extension = static::extension($path); + $glob = $dir . '/' . $name . $pattern . '.' . $extension; + return glob($glob); + } + + /** + * Returns the size of a file or an array of files. + * + * @param string|array $file file path or array of paths + * @return int + */ + public static function size($file): int + { + if (is_array($file) === true) { + return array_reduce( + $file, + fn ($total, $file) => $total + F::size($file), + 0 + ); + } + + try { + return filesize($file); + } catch (Throwable $e) { + return 0; + } + } + + /** + * Categorize the file + * + * @param string $file Either the file path or extension + * @return string|null + */ + public static function type(string $file) + { + $length = strlen($file); + + if ($length >= 2 && $length <= 4) { + // use the file name as extension + $extension = $file; + } else { + // get the extension from the filename + $extension = pathinfo($file, PATHINFO_EXTENSION); + } + + if (empty($extension) === true) { + // detect the mime type first to get the most reliable extension + $mime = static::mime($file); + $extension = static::mimeToExtension($mime); + } + + // sanitize extension + $extension = strtolower($extension); + + foreach (static::$types as $type => $extensions) { + if (in_array($extension, $extensions) === true) { + return $type; + } + } + + return null; + } + + /** + * Returns all extensions of a given file type + * or `null` if the file type is unknown + * + * @param string $type + * @return array|null + */ + public static function typeToExtensions(string $type): ?array + { + return static::$types[$type] ?? null; + } + + /** + * Ensures that a file or link is deleted (with race condition handling) + * @since 3.7.4 + */ + public static function unlink(string $file): bool + { + return Helpers::handleErrors( + fn (): bool => unlink($file), + function (&$override, int $errno, string $errstr): bool { + // if the file or link was already deleted (race condition), + // consider it a success + if (Str::endsWith($errstr, 'No such file or directory') === true) { + $override = true; + + // drop the warning + return true; + } + + // handle every other warning normally + return false; + } + ); + } + + /** + * Unzips a zip file + * + * @param string $file + * @param string $to + * @return bool + */ + public static function unzip(string $file, string $to): bool + { + if (class_exists('ZipArchive') === false) { + throw new Exception('The ZipArchive class is not available'); + } + + $zip = new ZipArchive(); + + if ($zip->open($file) === true) { + $zip->extractTo($to); + $zip->close(); + return true; + } + + return false; + } + + /** + * Returns the file as data uri + * + * @param string $file The path for the file + * @return string|false + */ + public static function uri(string $file) + { + if ($mime = static::mime($file)) { + return 'data:' . $mime . ';base64,' . static::base64($file); + } + + return false; + } + + /** + * Creates a new file + * + * @param string $file The path for the new file + * @param mixed $content Either a string, an object or an array. Arrays and objects will be serialized. + * @param bool $append true: append the content to an existing file if available. false: overwrite. + * @return bool + */ + public static function write(string $file, $content, bool $append = false): bool + { + if (is_array($content) === true || is_object($content) === true) { + $content = serialize($content); + } + + $mode = $append === true ? FILE_APPEND | LOCK_EX : LOCK_EX; + + // if the parent directory does not exist, create it + if (is_dir(dirname($file)) === false) { + if (Dir::make(dirname($file)) === false) { + return false; + } + } + + if (static::isWritable($file) === false) { + throw new Exception('The file "' . $file . '" is not writable'); + } + + return file_put_contents($file, $content, $mode) !== false; + } } diff --git a/kirby/src/Filesystem/File.php b/kirby/src/Filesystem/File.php index 751bf4d..89cffbb 100644 --- a/kirby/src/Filesystem/File.php +++ b/kirby/src/Filesystem/File.php @@ -25,610 +25,610 @@ use Kirby\Toolkit\V; */ class File { - use Properties; + use Properties; - /** - * Absolute file path - * - * @var string - */ - protected $root; + /** + * Absolute file path + * + * @var string + */ + protected $root; - /** - * Absolute file URL - * - * @var string|null - */ - protected $url; + /** + * Absolute file URL + * + * @var string|null + */ + protected $url; - /** - * Validation rules to be used for `::match()` - * - * @var array - */ - public static $validations = [ - 'maxsize' => ['size', 'max'], - 'minsize' => ['size', 'min'] - ]; + /** + * Validation rules to be used for `::match()` + * + * @var array + */ + public static $validations = [ + 'maxsize' => ['size', 'max'], + 'minsize' => ['size', 'min'] + ]; - /** - * Constructor sets all file properties - * - * @param array|string|null $props Properties or deprecated `$root` string - * @param string|null $url Deprecated argument, use `$props['url']` instead - */ - public function __construct($props = null, string $url = null) - { - // Legacy support for old constructor of - // the `Kirby\Image\Image` class - // @todo 4.0.0 remove - if (is_array($props) === false) { - $props = [ - 'root' => $props, - 'url' => $url - ]; - } + /** + * Constructor sets all file properties + * + * @param array|string|null $props Properties or deprecated `$root` string + * @param string|null $url Deprecated argument, use `$props['url']` instead + */ + public function __construct($props = null, string $url = null) + { + // Legacy support for old constructor of + // the `Kirby\Image\Image` class + // @todo 4.0.0 remove + if (is_array($props) === false) { + $props = [ + 'root' => $props, + 'url' => $url + ]; + } - $this->setProperties($props); - } + $this->setProperties($props); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * Returns the URL for the file object - * - * @return string - */ - public function __toString(): string - { - return $this->url() ?? $this->root() ?? ''; - } + /** + * Returns the URL for the file object + * + * @return string + */ + public function __toString(): string + { + return $this->url() ?? $this->root() ?? ''; + } - /** - * Returns the file content as base64 encoded string - * - * @return string - */ - public function base64(): string - { - return base64_encode($this->read()); - } + /** + * Returns the file content as base64 encoded string + * + * @return string + */ + public function base64(): string + { + return base64_encode($this->read()); + } - /** - * Copy a file to a new location. - * - * @param string $target - * @param bool $force - * @return static - */ - public function copy(string $target, bool $force = false) - { - if (F::copy($this->root, $target, $force) !== true) { - throw new Exception('The file "' . $this->root . '" could not be copied'); - } + /** + * Copy a file to a new location. + * + * @param string $target + * @param bool $force + * @return static + */ + public function copy(string $target, bool $force = false) + { + if (F::copy($this->root, $target, $force) !== true) { + throw new Exception('The file "' . $this->root . '" could not be copied'); + } - return new static($target); - } + return new static($target); + } - /** - * Returns the file as data uri - * - * @param bool $base64 Whether the data should be base64 encoded or not - * @return string - */ - public function dataUri(bool $base64 = true): string - { - if ($base64 === true) { - return 'data:' . $this->mime() . ';base64,' . $this->base64(); - } + /** + * Returns the file as data uri + * + * @param bool $base64 Whether the data should be base64 encoded or not + * @return string + */ + public function dataUri(bool $base64 = true): string + { + if ($base64 === true) { + return 'data:' . $this->mime() . ';base64,' . $this->base64(); + } - return 'data:' . $this->mime() . ',' . Escape::url($this->read()); - } + return 'data:' . $this->mime() . ',' . Escape::url($this->read()); + } - /** - * Deletes the file - * - * @return bool - */ - public function delete(): bool - { - if (F::remove($this->root) !== true) { - throw new Exception('The file "' . $this->root . '" could not be deleted'); - } + /** + * Deletes the file + * + * @return bool + */ + public function delete(): bool + { + if (F::remove($this->root) !== true) { + throw new Exception('The file "' . $this->root . '" could not be deleted'); + } - return true; - } + return true; + } - /* - * Automatically sends all needed headers for the file to be downloaded - * and echos the file's content - * - * @param string|null $filename Optional filename for the download - * @return string - */ - public function download($filename = null): string - { - return Response::download($this->root, $filename ?? $this->filename()); - } + /* + * Automatically sends all needed headers for the file to be downloaded + * and echos the file's content + * + * @param string|null $filename Optional filename for the download + * @return string + */ + public function download($filename = null): string + { + return Response::download($this->root, $filename ?? $this->filename()); + } - /** - * Checks if the file actually exists - * - * @return bool - */ - public function exists(): bool - { - return file_exists($this->root) === true; - } + /** + * Checks if the file actually exists + * + * @return bool + */ + public function exists(): bool + { + return file_exists($this->root) === true; + } - /** - * Returns the current lowercase extension (without .) - * - * @return string - */ - public function extension(): string - { - return F::extension($this->root); - } + /** + * Returns the current lowercase extension (without .) + * + * @return string + */ + public function extension(): string + { + return F::extension($this->root); + } - /** - * Returns the filename - * - * @return string - */ - public function filename(): string - { - return basename($this->root); - } + /** + * Returns the filename + * + * @return string + */ + public function filename(): string + { + return basename($this->root); + } - /** - * Returns a md5 hash of the root - * - * @return string - */ - public function hash(): string - { - return md5($this->root); - } + /** + * Returns a md5 hash of the root + * + * @return string + */ + public function hash(): string + { + return md5($this->root); + } - /** - * Sends an appropriate header for the asset - * - * @param bool $send - * @return \Kirby\Http\Response|void - */ - public function header(bool $send = true) - { - $response = new Response('', $this->mime()); + /** + * Sends an appropriate header for the asset + * + * @param bool $send + * @return \Kirby\Http\Response|void + */ + public function header(bool $send = true) + { + $response = new Response('', $this->mime()); - if ($send !== true) { - return $response; - } + if ($send !== true) { + return $response; + } - $response->send(); - } + $response->send(); + } - /** - * Converts the file to html - * - * @param array $attr - * @return string - */ - public function html(array $attr = []): string - { - return Html::a($this->url() ?? '', $attr); - } + /** + * Converts the file to html + * + * @param array $attr + * @return string + */ + public function html(array $attr = []): string + { + return Html::a($this->url() ?? '', $attr); + } - /** - * Checks if a file is of a certain type - * - * @param string $value An extension or mime type - * @return bool - */ - public function is(string $value): bool - { - return F::is($this->root, $value); - } + /** + * Checks if a file is of a certain type + * + * @param string $value An extension or mime type + * @return bool + */ + public function is(string $value): bool + { + return F::is($this->root, $value); + } - /** - * Checks if the file is readable - * - * @return bool - */ - public function isReadable(): bool - { - return is_readable($this->root) === true; - } + /** + * Checks if the file is readable + * + * @return bool + */ + public function isReadable(): bool + { + return is_readable($this->root) === true; + } - /** - * Checks if the file is a resizable image - * - * @return bool - */ - public function isResizable(): bool - { - return false; - } + /** + * Checks if the file is a resizable image + * + * @return bool + */ + public function isResizable(): bool + { + return false; + } - /** - * Checks if a preview can be displayed for the file - * in the panel or in the frontend - * - * @return bool - */ - public function isViewable(): bool - { - return false; - } + /** + * Checks if a preview can be displayed for the file + * in the panel or in the frontend + * + * @return bool + */ + public function isViewable(): bool + { + return false; + } - /** - * Checks if the file is writable - * - * @return bool - */ - public function isWritable(): bool - { - return F::isWritable($this->root); - } + /** + * Checks if the file is writable + * + * @return bool + */ + public function isWritable(): bool + { + return F::isWritable($this->root); + } - /** - * Returns the app instance if it exists - * - * @return \Kirby\Cms\App|null - */ - public function kirby() - { - return App::instance(null, true); - } + /** + * Returns the app instance if it exists + * + * @return \Kirby\Cms\App|null + */ + public function kirby() + { + return App::instance(null, true); + } - /** - * Runs a set of validations on the file object - * (mainly for images). - * - * @param array $rules - * @return bool - * @throws \Kirby\Exception\Exception - */ - public function match(array $rules): bool - { - $rules = array_change_key_case($rules); + /** + * Runs a set of validations on the file object + * (mainly for images). + * + * @param array $rules + * @return bool + * @throws \Kirby\Exception\Exception + */ + public function match(array $rules): bool + { + $rules = array_change_key_case($rules); - if (is_array($rules['mime'] ?? null) === true) { - $mime = $this->mime(); + if (is_array($rules['mime'] ?? null) === true) { + $mime = $this->mime(); - // determine if any pattern matches the MIME type; - // once any pattern matches, `$carry` is `true` and the rest is skipped - $matches = array_reduce( - $rules['mime'], - fn ($carry, $pattern) => $carry || Mime::matches($mime, $pattern), - false - ); + // determine if any pattern matches the MIME type; + // once any pattern matches, `$carry` is `true` and the rest is skipped + $matches = array_reduce( + $rules['mime'], + fn ($carry, $pattern) => $carry || Mime::matches($mime, $pattern), + false + ); - if ($matches !== true) { - throw new Exception([ - 'key' => 'file.mime.invalid', - 'data' => compact('mime') - ]); - } - } + if ($matches !== true) { + throw new Exception([ + 'key' => 'file.mime.invalid', + 'data' => compact('mime') + ]); + } + } - if (is_array($rules['extension'] ?? null) === true) { - $extension = $this->extension(); - if (in_array($extension, $rules['extension']) !== true) { - throw new Exception([ - 'key' => 'file.extension.invalid', - 'data' => compact('extension') - ]); - } - } + if (is_array($rules['extension'] ?? null) === true) { + $extension = $this->extension(); + if (in_array($extension, $rules['extension']) !== true) { + throw new Exception([ + 'key' => 'file.extension.invalid', + 'data' => compact('extension') + ]); + } + } - if (is_array($rules['type'] ?? null) === true) { - $type = $this->type(); - if (in_array($type, $rules['type']) !== true) { - throw new Exception([ - 'key' => 'file.type.invalid', - 'data' => compact('type') - ]); - } - } + if (is_array($rules['type'] ?? null) === true) { + $type = $this->type(); + if (in_array($type, $rules['type']) !== true) { + throw new Exception([ + 'key' => 'file.type.invalid', + 'data' => compact('type') + ]); + } + } - foreach (static::$validations as $key => $arguments) { - $rule = $rules[$key] ?? null; + foreach (static::$validations as $key => $arguments) { + $rule = $rules[$key] ?? null; - if ($rule !== null) { - $property = $arguments[0]; - $validator = $arguments[1]; + if ($rule !== null) { + $property = $arguments[0]; + $validator = $arguments[1]; - if (V::$validator($this->$property(), $rule) === false) { - throw new Exception([ - 'key' => 'file.' . $key, - 'data' => [$property => $rule] - ]); - } - } - } + if (V::$validator($this->$property(), $rule) === false) { + throw new Exception([ + 'key' => 'file.' . $key, + 'data' => [$property => $rule] + ]); + } + } + } - return true; - } + return true; + } - /** - * Detects the mime type of the file - * - * @return string|null - */ - public function mime() - { - return Mime::type($this->root); - } + /** + * Detects the mime type of the file + * + * @return string|null + */ + public function mime() + { + return Mime::type($this->root); + } - /** - * Returns the file's last modification time - * - * @param string $format - * @param string|null $handler date or strftime - * @return mixed - */ - public function modified(?string $format = null, ?string $handler = null) - { - $kirby = $this->kirby(); + /** + * Returns the file's last modification time + * + * @param string|\IntlDateFormatter|null $format + * @param string|null $handler date, intl or strftime + * @return mixed + */ + public function modified($format = null, ?string $handler = null) + { + $kirby = $this->kirby(); - return F::modified( - $this->root, - $format, - $handler ?? ($kirby ? $kirby->option('date.handler', 'date') : 'date') - ); - } + return F::modified( + $this->root, + $format, + $handler ?? ($kirby ? $kirby->option('date.handler', 'date') : 'date') + ); + } - /** - * Move the file to a new location - * - * @param string $newRoot - * @param bool $overwrite Force overwriting any existing files - * @return static - */ - public function move(string $newRoot, bool $overwrite = false) - { - if (F::move($this->root, $newRoot, $overwrite) !== true) { - throw new Exception('The file: "' . $this->root . '" could not be moved to: "' . $newRoot . '"'); - } + /** + * Move the file to a new location + * + * @param string $newRoot + * @param bool $overwrite Force overwriting any existing files + * @return static + */ + public function move(string $newRoot, bool $overwrite = false) + { + if (F::move($this->root, $newRoot, $overwrite) !== true) { + throw new Exception('The file: "' . $this->root . '" could not be moved to: "' . $newRoot . '"'); + } - return new static($newRoot); - } + return new static($newRoot); + } - /** - * Getter for the name of the file - * without the extension - * - * @return string - */ - public function name(): string - { - return pathinfo($this->root, PATHINFO_FILENAME); - } + /** + * Getter for the name of the file + * without the extension + * + * @return string + */ + public function name(): string + { + return pathinfo($this->root, PATHINFO_FILENAME); + } - /** - * Returns the file size in a - * human-readable format - * - * @param string|null|false $locale Locale for number formatting, - * `null` for the current locale, - * `false` to disable number formatting - * @return string - */ - public function niceSize($locale = null): string - { - return F::niceSize($this->root, $locale); - } + /** + * Returns the file size in a + * human-readable format + * + * @param string|null|false $locale Locale for number formatting, + * `null` for the current locale, + * `false` to disable number formatting + * @return string + */ + public function niceSize($locale = null): string + { + return F::niceSize($this->root, $locale); + } - /** - * Reads the file content and returns it. - * - * @return string|false - */ - public function read() - { - return F::read($this->root); - } + /** + * Reads the file content and returns it. + * + * @return string|false + */ + public function read() + { + return F::read($this->root); + } - /** - * Returns the absolute path to the file - * - * @return string - */ - public function realpath(): string - { - return realpath($this->root); - } + /** + * Returns the absolute path to the file + * + * @return string + */ + public function realpath(): string + { + return realpath($this->root); + } - /** - * Changes the name of the file without - * touching the extension - * - * @param string $newName - * @param bool $overwrite Force overwrite existing files - * @return static - */ - public function rename(string $newName, bool $overwrite = false) - { - $newRoot = F::rename($this->root, $newName, $overwrite); + /** + * Changes the name of the file without + * touching the extension + * + * @param string $newName + * @param bool $overwrite Force overwrite existing files + * @return static + */ + public function rename(string $newName, bool $overwrite = false) + { + $newRoot = F::rename($this->root, $newName, $overwrite); - if ($newRoot === false) { - throw new Exception('The file: "' . $this->root . '" could not be renamed to: "' . $newName . '"'); - } + if ($newRoot === false) { + throw new Exception('The file: "' . $this->root . '" could not be renamed to: "' . $newName . '"'); + } - return new static($newRoot); - } + return new static($newRoot); + } - /** - * Returns the given file path - * - * @return string|null - */ - public function root(): ?string - { - return $this->root; - } + /** + * Returns the given file path + * + * @return string|null + */ + public function root(): ?string + { + return $this->root; + } - /** - * Setter for the root - * - * @param string|null $root - * @return $this - */ - protected function setRoot(?string $root = null) - { - $this->root = $root; - return $this; - } + /** + * Setter for the root + * + * @param string|null $root + * @return $this + */ + protected function setRoot(?string $root = null) + { + $this->root = $root; + return $this; + } - /** - * Setter for the file url - * - * @param string|null $url - * @return $this - */ - protected function setUrl(?string $url = null) - { - $this->url = $url; - return $this; - } + /** + * Setter for the file url + * + * @param string|null $url + * @return $this + */ + protected function setUrl(?string $url = null) + { + $this->url = $url; + return $this; + } - /** - * Returns the absolute url for the file - * - * @return string|null - */ - public function url(): ?string - { - return $this->url; - } + /** + * Returns the absolute url for the file + * + * @return string|null + */ + public function url(): ?string + { + return $this->url; + } - /** - * Sanitizes the file contents depending on the file type - * by overwriting the file with the sanitized version - * @since 3.6.0 - * - * @param string|bool $typeLazy Explicit sane handler type string, - * `true` for lazy autodetection or - * `false` for normal autodetection - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\LogicException If more than one handler applies - * @throws \Kirby\Exception\NotFoundException If the handler was not found - * @throws \Kirby\Exception\Exception On other errors - */ - public function sanitizeContents($typeLazy = false): void - { - Sane::sanitizeFile($this->root(), $typeLazy); - } + /** + * Sanitizes the file contents depending on the file type + * by overwriting the file with the sanitized version + * @since 3.6.0 + * + * @param string|bool $typeLazy Explicit sane handler type string, + * `true` for lazy autodetection or + * `false` for normal autodetection + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\LogicException If more than one handler applies + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public function sanitizeContents($typeLazy = false): void + { + Sane::sanitizeFile($this->root(), $typeLazy); + } - /** - * Returns the sha1 hash of the file - * @since 3.6.0 - * - * @return string - */ - public function sha1(): string - { - return sha1_file($this->root); - } + /** + * Returns the sha1 hash of the file + * @since 3.6.0 + * + * @return string + */ + public function sha1(): string + { + return sha1_file($this->root); + } - /** - * Returns the raw size of the file - * - * @return int - */ - public function size(): int - { - return F::size($this->root); - } + /** + * Returns the raw size of the file + * + * @return int + */ + public function size(): int + { + return F::size($this->root); + } - /** - * Converts the media object to a - * plain PHP array - * - * @return array - */ - public function toArray(): array - { - return [ - 'extension' => $this->extension(), - 'filename' => $this->filename(), - 'hash' => $this->hash(), - 'isReadable' => $this->isReadable(), - 'isResizable' => $this->isResizable(), - 'isWritable' => $this->isWritable(), - 'mime' => $this->mime(), - 'modified' => $this->modified('c'), - 'name' => $this->name(), - 'niceSize' => $this->niceSize(), - 'root' => $this->root(), - 'safeName' => F::safeName($this->name()), - 'size' => $this->size(), - 'type' => $this->type(), - 'url' => $this->url() - ]; - } + /** + * Converts the media object to a + * plain PHP array + * + * @return array + */ + public function toArray(): array + { + return [ + 'extension' => $this->extension(), + 'filename' => $this->filename(), + 'hash' => $this->hash(), + 'isReadable' => $this->isReadable(), + 'isResizable' => $this->isResizable(), + 'isWritable' => $this->isWritable(), + 'mime' => $this->mime(), + 'modified' => $this->modified('c'), + 'name' => $this->name(), + 'niceSize' => $this->niceSize(), + 'root' => $this->root(), + 'safeName' => F::safeName($this->name()), + 'size' => $this->size(), + 'type' => $this->type(), + 'url' => $this->url() + ]; + } - /** - * Converts the entire file array into - * a json string - * - * @return string - */ - public function toJson(): string - { - return json_encode($this->toArray()); - } + /** + * Converts the entire file array into + * a json string + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->toArray()); + } - /** - * Returns the file type. - * - * @return string|null - */ - public function type(): ?string - { - return F::type($this->root); - } + /** + * Returns the file type. + * + * @return string|null + */ + public function type(): ?string + { + return F::type($this->root); + } - /** - * Validates the file contents depending on the file type - * - * @param string|bool $typeLazy Explicit sane handler type string, - * `true` for lazy autodetection or - * `false` for normal autodetection - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\NotFoundException If the handler was not found - * @throws \Kirby\Exception\Exception On other errors - */ - public function validateContents($typeLazy = false): void - { - Sane::validateFile($this->root(), $typeLazy); - } + /** + * Validates the file contents depending on the file type + * + * @param string|bool $typeLazy Explicit sane handler type string, + * `true` for lazy autodetection or + * `false` for normal autodetection + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public function validateContents($typeLazy = false): void + { + Sane::validateFile($this->root(), $typeLazy); + } - /** - * Writes content to the file - * - * @param string $content - * @return bool - */ - public function write($content): bool - { - if (F::write($this->root, $content) !== true) { - throw new Exception('The file "' . $this->root . '" could not be written'); - } + /** + * Writes content to the file + * + * @param string $content + * @return bool + */ + public function write($content): bool + { + if (F::write($this->root, $content) !== true) { + throw new Exception('The file "' . $this->root . '" could not be written'); + } - return true; - } + return true; + } } diff --git a/kirby/src/Filesystem/Filename.php b/kirby/src/Filesystem/Filename.php index 01c1647..f97de71 100644 --- a/kirby/src/Filesystem/Filename.php +++ b/kirby/src/Filesystem/Filename.php @@ -28,280 +28,280 @@ use Kirby\Toolkit\Str; */ class Filename { - /** - * List of all applicable attributes - * - * @var array - */ - protected $attributes; + /** + * List of all applicable attributes + * + * @var array + */ + protected $attributes; - /** - * The sanitized file extension - * - * @var string - */ - protected $extension; + /** + * The sanitized file extension + * + * @var string + */ + protected $extension; - /** - * The source original filename - * - * @var string - */ - protected $filename; + /** + * The source original filename + * + * @var string + */ + protected $filename; - /** - * The sanitized file name - * - * @var string - */ - protected $name; + /** + * The sanitized file name + * + * @var string + */ + protected $name; - /** - * The template for the final name - * - * @var string - */ - protected $template; + /** + * The template for the final name + * + * @var string + */ + protected $template; - /** - * Creates a new Filename object - * - * @param string $filename - * @param string $template - * @param array $attributes - */ - public function __construct(string $filename, string $template, array $attributes = []) - { - $this->filename = $filename; - $this->template = $template; - $this->attributes = $attributes; - $this->extension = $this->sanitizeExtension( - $attributes['format'] ?? - pathinfo($filename, PATHINFO_EXTENSION) - ); - $this->name = $this->sanitizeName(pathinfo($filename, PATHINFO_FILENAME)); - } + /** + * Creates a new Filename object + * + * @param string $filename + * @param string $template + * @param array $attributes + */ + public function __construct(string $filename, string $template, array $attributes = []) + { + $this->filename = $filename; + $this->template = $template; + $this->attributes = $attributes; + $this->extension = $this->sanitizeExtension( + $attributes['format'] ?? + pathinfo($filename, PATHINFO_EXTENSION) + ); + $this->name = $this->sanitizeName(pathinfo($filename, PATHINFO_FILENAME)); + } - /** - * Converts the entire object to a string - * - * @return string - */ - public function __toString(): string - { - return $this->toString(); - } + /** + * Converts the entire object to a string + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } - /** - * Converts all processed attributes - * to an array. The array keys are already - * the shortened versions for the filename - * - * @return array - */ - public function attributesToArray(): array - { - $array = [ - 'dimensions' => implode('x', $this->dimensions()), - 'crop' => $this->crop(), - 'blur' => $this->blur(), - 'bw' => $this->grayscale(), - 'q' => $this->quality(), - ]; + /** + * Converts all processed attributes + * to an array. The array keys are already + * the shortened versions for the filename + * + * @return array + */ + public function attributesToArray(): array + { + $array = [ + 'dimensions' => implode('x', $this->dimensions()), + 'crop' => $this->crop(), + 'blur' => $this->blur(), + 'bw' => $this->grayscale(), + 'q' => $this->quality(), + ]; - $array = array_filter( - $array, - fn ($item) => $item !== null && $item !== false && $item !== '' - ); + $array = array_filter( + $array, + fn ($item) => $item !== null && $item !== false && $item !== '' + ); - return $array; - } + return $array; + } - /** - * Converts all processed attributes - * to a string, that can be used in the - * new filename - * - * @param string|null $prefix The prefix will be used in the filename creation - * @return string - */ - public function attributesToString(string $prefix = null): string - { - $array = $this->attributesToArray(); - $result = []; + /** + * Converts all processed attributes + * to a string, that can be used in the + * new filename + * + * @param string|null $prefix The prefix will be used in the filename creation + * @return string + */ + public function attributesToString(string $prefix = null): string + { + $array = $this->attributesToArray(); + $result = []; - foreach ($array as $key => $value) { - if ($value === true) { - $value = ''; - } + foreach ($array as $key => $value) { + if ($value === true) { + $value = ''; + } - switch ($key) { - case 'dimensions': - $result[] = $value; - break; - case 'crop': - $result[] = ($value === 'center') ? 'crop' : $key . '-' . $value; - break; - default: - $result[] = $key . $value; - } - } + switch ($key) { + case 'dimensions': + $result[] = $value; + break; + case 'crop': + $result[] = ($value === 'center') ? 'crop' : $key . '-' . $value; + break; + default: + $result[] = $key . $value; + } + } - $result = array_filter($result); - $attributes = implode('-', $result); + $result = array_filter($result); + $attributes = implode('-', $result); - if (empty($attributes) === true) { - return ''; - } + if (empty($attributes) === true) { + return ''; + } - return $prefix . $attributes; - } + return $prefix . $attributes; + } - /** - * Normalizes the blur option value - * - * @return false|int - */ - public function blur() - { - $value = $this->attributes['blur'] ?? false; + /** + * Normalizes the blur option value + * + * @return false|int + */ + public function blur() + { + $value = $this->attributes['blur'] ?? false; - if ($value === false) { - return false; - } + if ($value === false) { + return false; + } - return (int)$value; - } + return (int)$value; + } - /** - * Normalizes the crop option value - * - * @return false|string - */ - public function crop() - { - // get the crop value - $crop = $this->attributes['crop'] ?? false; + /** + * Normalizes the crop option value + * + * @return false|string + */ + public function crop() + { + // get the crop value + $crop = $this->attributes['crop'] ?? false; - if ($crop === false) { - return false; - } + if ($crop === false) { + return false; + } - return Str::slug($crop); - } + return Str::slug($crop); + } - /** - * Returns a normalized array - * with width and height values - * if available - * - * @return array - */ - public function dimensions() - { - if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) { - return []; - } + /** + * Returns a normalized array + * with width and height values + * if available + * + * @return array + */ + public function dimensions() + { + if (empty($this->attributes['width']) === true && empty($this->attributes['height']) === true) { + return []; + } - return [ - 'width' => $this->attributes['width'] ?? null, - 'height' => $this->attributes['height'] ?? null - ]; - } + return [ + 'width' => $this->attributes['width'] ?? null, + 'height' => $this->attributes['height'] ?? null + ]; + } - /** - * Returns the sanitized extension - * - * @return string - */ - public function extension(): string - { - return $this->extension; - } + /** + * Returns the sanitized extension + * + * @return string + */ + public function extension(): string + { + return $this->extension; + } - /** - * Normalizes the grayscale option value - * and also the available ways to write - * the option. You can use `grayscale`, - * `greyscale` or simply `bw`. The function - * will always return `grayscale` - * - * @return bool - */ - public function grayscale(): bool - { - // normalize options - $value = $this->attributes['grayscale'] ?? $this->attributes['greyscale'] ?? $this->attributes['bw'] ?? false; + /** + * Normalizes the grayscale option value + * and also the available ways to write + * the option. You can use `grayscale`, + * `greyscale` or simply `bw`. The function + * will always return `grayscale` + * + * @return bool + */ + public function grayscale(): bool + { + // normalize options + $value = $this->attributes['grayscale'] ?? $this->attributes['greyscale'] ?? $this->attributes['bw'] ?? false; - // turn anything into boolean - return filter_var($value, FILTER_VALIDATE_BOOLEAN); - } + // turn anything into boolean + return filter_var($value, FILTER_VALIDATE_BOOLEAN); + } - /** - * Returns the filename without extension - * - * @return string - */ - public function name(): string - { - return $this->name; - } + /** + * Returns the filename without extension + * + * @return string + */ + public function name(): string + { + return $this->name; + } - /** - * Normalizes the quality option value - * - * @return false|int - */ - public function quality() - { - $value = $this->attributes['quality'] ?? false; + /** + * Normalizes the quality option value + * + * @return false|int + */ + public function quality() + { + $value = $this->attributes['quality'] ?? false; - if ($value === false || $value === true) { - return false; - } + if ($value === false || $value === true) { + return false; + } - return (int)$value; - } + return (int)$value; + } - /** - * Sanitizes the file extension. - * The extension will be converted - * to lowercase and `jpeg` will be - * replaced with `jpg` - * - * @param string $extension - * @return string - */ - protected function sanitizeExtension(string $extension): string - { - $extension = strtolower($extension); - $extension = str_replace('jpeg', 'jpg', $extension); - return $extension; - } + /** + * Sanitizes the file extension. + * The extension will be converted + * to lowercase and `jpeg` will be + * replaced with `jpg` + * + * @param string $extension + * @return string + */ + protected function sanitizeExtension(string $extension): string + { + $extension = strtolower($extension); + $extension = str_replace('jpeg', 'jpg', $extension); + return $extension; + } - /** - * Sanitizes the name with Kirby's - * Str::slug function - * - * @param string $name - * @return string - */ - protected function sanitizeName(string $name): string - { - return Str::slug($name); - } + /** + * Sanitizes the name with Kirby's + * Str::slug function + * + * @param string $name + * @return string + */ + protected function sanitizeName(string $name): string + { + return Str::slug($name); + } - /** - * Returns the converted filename as string - * - * @return string - */ - public function toString(): string - { - return Str::template($this->template, [ - 'name' => $this->name(), - 'attributes' => $this->attributesToString('-'), - 'extension' => $this->extension() - ], ['fallback' => '']); - } + /** + * Returns the converted filename as string + * + * @return string + */ + public function toString(): string + { + return Str::template($this->template, [ + 'name' => $this->name(), + 'attributes' => $this->attributesToString('-'), + 'extension' => $this->extension() + ], ['fallback' => '']); + } } diff --git a/kirby/src/Filesystem/IsFile.php b/kirby/src/Filesystem/IsFile.php index 8162f3c..7e69799 100644 --- a/kirby/src/Filesystem/IsFile.php +++ b/kirby/src/Filesystem/IsFile.php @@ -22,175 +22,175 @@ use Kirby\Toolkit\Properties; */ trait IsFile { - use Properties; + use Properties; - /** - * File asset object - * - * @var \Kirby\Filesystem\File - */ - protected $asset; + /** + * File asset object + * + * @var \Kirby\Filesystem\File + */ + protected $asset; - /** - * Absolute file path - * - * @var string|null - */ - protected $root; + /** + * Absolute file path + * + * @var string|null + */ + protected $root; - /** - * Absolute file URL - * - * @var string|null - */ - protected $url; + /** + * Absolute file URL + * + * @var string|null + */ + protected $url; - /** - * Constructor sets all file properties - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setProperties($props); - } + /** + * Constructor sets all file properties + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } - /** - * Magic caller for asset methods - * - * @param string $method - * @param array $arguments - * @return mixed - * @throws \Kirby\Exception\BadMethodCallException - */ - public function __call(string $method, array $arguments = []) - { - // public property access - if (isset($this->$method) === true) { - return $this->$method; - } + /** + * Magic caller for asset methods + * + * @param string $method + * @param array $arguments + * @return mixed + * @throws \Kirby\Exception\BadMethodCallException + */ + public function __call(string $method, array $arguments = []) + { + // public property access + if (isset($this->$method) === true) { + return $this->$method; + } - // asset method proxy - if (method_exists($this->asset(), $method)) { - return $this->asset()->$method(...$arguments); - } + // asset method proxy + if (method_exists($this->asset(), $method)) { + return $this->asset()->$method(...$arguments); + } - throw new BadMethodCallException('The method: "' . $method . '" does not exist'); - } + throw new BadMethodCallException('The method: "' . $method . '" does not exist'); + } - /** - * Converts the asset to a string - * - * @return string - */ - public function __toString(): string - { - return (string)$this->asset(); - } + /** + * Converts the asset to a string + * + * @return string + */ + public function __toString(): string + { + return (string)$this->asset(); + } - /** - * Returns the file asset object - * - * @param array|string|null $props - * @return \Kirby\Filesystem\File - */ - public function asset($props = null) - { - if ($this->asset !== null) { - return $this->asset; - } + /** + * Returns the file asset object + * + * @param array|string|null $props + * @return \Kirby\Filesystem\File + */ + public function asset($props = null) + { + if ($this->asset !== null) { + return $this->asset; + } - $props = $props ?? [ - 'root' => $this->root(), - 'url' => $this->url() - ]; + $props = $props ?? [ + 'root' => $this->root(), + 'url' => $this->url() + ]; - switch ($this->type()) { - case 'image': - return $this->asset = new Image($props); - default: - return $this->asset = new File($props); - } - } + switch ($this->type()) { + case 'image': + return $this->asset = new Image($props); + default: + return $this->asset = new File($props); + } + } - /** - * Checks if the file exists on disk - * - * @return bool - */ - public function exists(): bool - { - // Important to include this in the trait - // to avoid infinite loops when trying - // to proxy the method from the asset object - return file_exists($this->root()) === true; - } + /** + * Checks if the file exists on disk + * + * @return bool + */ + public function exists(): bool + { + // Important to include this in the trait + // to avoid infinite loops when trying + // to proxy the method from the asset object + return file_exists($this->root()) === true; + } - /** - * Returns the app instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return App::instance(); - } + /** + * Returns the app instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return App::instance(); + } - /** - * Returns the given file path - * - * @return string|null - */ - public function root(): ?string - { - return $this->root; - } + /** + * Returns the given file path + * + * @return string|null + */ + public function root(): ?string + { + return $this->root; + } - /** - * Setter for the root - * - * @param string|null $root - * @return $this - */ - protected function setRoot(?string $root = null) - { - $this->root = $root; - return $this; - } + /** + * Setter for the root + * + * @param string|null $root + * @return $this + */ + protected function setRoot(?string $root = null) + { + $this->root = $root; + return $this; + } - /** - * Setter for the file url - * - * @param string|null $url - * @return $this - */ - protected function setUrl(?string $url = null) - { - $this->url = $url; - return $this; - } + /** + * Setter for the file url + * + * @param string|null $url + * @return $this + */ + protected function setUrl(?string $url = null) + { + $this->url = $url; + return $this; + } - /** - * Returns the file type - * - * @return string|null - */ - public function type(): ?string - { - // Important to include this in the trait - // to avoid infinite loops when trying - // to proxy the method from the asset object - return F::type($this->root() ?? $this->url()); - } + /** + * Returns the file type + * + * @return string|null + */ + public function type(): ?string + { + // Important to include this in the trait + // to avoid infinite loops when trying + // to proxy the method from the asset object + return F::type($this->root() ?? $this->url()); + } - /** - * Returns the absolute url for the file - * - * @return string|null - */ - public function url(): ?string - { - return $this->url; - } + /** + * Returns the absolute url for the file + * + * @return string|null + */ + public function url(): ?string + { + return $this->url; + } } diff --git a/kirby/src/Filesystem/Mime.php b/kirby/src/Filesystem/Mime.php index ed153f9..ca8b63f 100644 --- a/kirby/src/Filesystem/Mime.php +++ b/kirby/src/Filesystem/Mime.php @@ -19,325 +19,333 @@ use SimpleXMLElement; */ class Mime { - /** - * Extension to MIME type map - * - * @var array - */ - public static $types = [ - 'ai' => 'application/postscript', - 'aif' => 'audio/x-aiff', - 'aifc' => 'audio/x-aiff', - 'aiff' => 'audio/x-aiff', - 'avi' => 'video/x-msvideo', - 'avif' => 'image/avif', - 'bmp' => 'image/bmp', - 'css' => 'text/css', - 'csv' => ['text/csv', 'text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'], - 'doc' => 'application/msword', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', - 'dvi' => 'application/x-dvi', - 'eml' => 'message/rfc822', - 'eps' => 'application/postscript', - 'exe' => ['application/octet-stream', 'application/x-msdownload'], - 'gif' => 'image/gif', - 'gtar' => 'application/x-gtar', - 'gz' => 'application/x-gzip', - 'htm' => 'text/html', - 'html' => 'text/html', - 'ico' => 'image/x-icon', - 'ics' => 'text/calendar', - 'js' => ['application/javascript', 'application/x-javascript'], - 'json' => ['application/json', 'text/json'], - 'j2k' => ['image/jp2'], - 'jp2' => ['image/jp2'], - 'jpg' => ['image/jpeg', 'image/pjpeg'], - 'jpeg' => ['image/jpeg', 'image/pjpeg'], - 'jpe' => ['image/jpeg', 'image/pjpeg'], - 'log' => ['text/plain', 'text/x-log'], - 'm4a' => 'audio/mp4', - 'm4v' => 'video/mp4', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mif' => 'application/vnd.mif', - 'mov' => 'video/quicktime', - 'movie' => 'video/x-sgi-movie', - 'mp2' => 'audio/mpeg', - 'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'], - 'mp4' => 'video/mp4', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mpga' => 'audio/mpeg', - 'odc' => 'application/vnd.oasis.opendocument.chart', - 'odp' => 'application/vnd.oasis.opendocument.presentation', - 'odt' => 'application/vnd.oasis.opendocument.text', - 'pdf' => ['application/pdf', 'application/x-download'], - 'php' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], - 'php3' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], - 'phps' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], - 'phtml' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], - 'png' => 'image/png', - 'ppt' => ['application/powerpoint', 'application/vnd.ms-powerpoint'], - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', - 'ps' => 'application/postscript', - 'psd' => 'application/x-photoshop', - 'qt' => 'video/quicktime', - 'rss' => 'application/rss+xml', - 'rtf' => 'text/rtf', - 'rtx' => 'text/richtext', - 'shtml' => 'text/html', - 'svg' => 'image/svg+xml', - 'swf' => 'application/x-shockwave-flash', - 'tar' => 'application/x-tar', - 'text' => 'text/plain', - 'txt' => 'text/plain', - 'tgz' => ['application/x-tar', 'application/x-gzip-compressed'], - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'wav' => 'audio/x-wav', - 'wbxml' => 'application/wbxml', - 'webm' => 'video/webm', - 'webp' => 'image/webp', - 'word' => ['application/msword', 'application/octet-stream'], - 'xhtml' => 'application/xhtml+xml', - 'xht' => 'application/xhtml+xml', - 'xml' => 'text/xml', - 'xl' => 'application/excel', - 'xls' => ['application/excel', 'application/vnd.ms-excel', 'application/msexcel'], - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', - 'xsl' => 'text/xml', - 'yaml' => ['application/yaml', 'text/yaml'], - 'yml' => ['application/yaml', 'text/yaml'], - 'zip' => ['application/x-zip', 'application/zip', 'application/x-zip-compressed'], - ]; + /** + * Extension to MIME type map + * + * @var array + */ + public static $types = [ + 'ai' => 'application/postscript', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'avi' => 'video/x-msvideo', + 'avif' => 'image/avif', + 'bmp' => 'image/bmp', + 'css' => 'text/css', + 'csv' => ['text/csv', 'text/x-comma-separated-values', 'text/comma-separated-values', 'application/octet-stream'], + 'doc' => 'application/msword', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dvi' => 'application/x-dvi', + 'eml' => 'message/rfc822', + 'eps' => 'application/postscript', + 'exe' => ['application/octet-stream', 'application/x-msdownload'], + 'gif' => 'image/gif', + 'gtar' => 'application/x-gtar', + 'gz' => 'application/x-gzip', + 'htm' => 'text/html', + 'html' => 'text/html', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'js' => ['application/javascript', 'application/x-javascript'], + 'json' => ['application/json', 'text/json'], + 'j2k' => ['image/jp2'], + 'jp2' => ['image/jp2'], + 'jpg' => ['image/jpeg', 'image/pjpeg'], + 'jpeg' => ['image/jpeg', 'image/pjpeg'], + 'jpe' => ['image/jpeg', 'image/pjpeg'], + 'log' => ['text/plain', 'text/x-log'], + 'm4a' => 'audio/mp4', + 'm4v' => 'video/mp4', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mif' => 'application/vnd.mif', + 'mjs' => 'text/javascript', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp3' => ['audio/mpeg', 'audio/mpg', 'audio/mpeg3', 'audio/mp3'], + 'mp4' => 'video/mp4', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpga' => 'audio/mpeg', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'pdf' => ['application/pdf', 'application/x-download'], + 'php' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], + 'php3' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], + 'phps' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], + 'phtml' => ['text/php', 'text/x-php', 'application/x-httpd-php', 'application/php', 'application/x-php', 'application/x-httpd-php-source'], + 'png' => 'image/png', + 'ppt' => ['application/powerpoint', 'application/vnd.ms-powerpoint'], + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ps' => 'application/postscript', + 'psd' => 'application/x-photoshop', + 'qt' => 'video/quicktime', + 'rss' => 'application/rss+xml', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'shtml' => 'text/html', + 'svg' => 'image/svg+xml', + 'swf' => 'application/x-shockwave-flash', + 'tar' => 'application/x-tar', + 'text' => 'text/plain', + 'txt' => 'text/plain', + 'tgz' => ['application/x-tar', 'application/x-gzip-compressed'], + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'wav' => 'audio/x-wav', + 'wbxml' => 'application/wbxml', + 'webm' => 'video/webm', + 'webp' => 'image/webp', + 'word' => ['application/msword', 'application/octet-stream'], + 'xhtml' => 'application/xhtml+xml', + 'xht' => 'application/xhtml+xml', + 'xml' => 'text/xml', + 'xl' => 'application/excel', + 'xls' => ['application/excel', 'application/vnd.ms-excel', 'application/msexcel'], + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xsl' => 'text/xml', + 'yaml' => ['application/yaml', 'text/yaml'], + 'yml' => ['application/yaml', 'text/yaml'], + 'zip' => ['application/x-zip', 'application/zip', 'application/x-zip-compressed'], + ]; - /** - * Fixes an invalid MIME type guess for the given file - * - * @param string $file - * @param string $mime - * @param string $extension - * @return string|null - */ - public static function fix(string $file, string $mime = null, string $extension = null) - { - // fixing map - $map = [ - 'text/html' => [ - 'svg' => ['Kirby\Filesystem\Mime', 'fromSvg'], - ], - 'text/plain' => [ - 'css' => 'text/css', - 'json' => 'application/json', - 'svg' => ['Kirby\Filesystem\Mime', 'fromSvg'], - ], - 'text/x-asm' => [ - 'css' => 'text/css' - ], - 'image/svg' => [ - 'svg' => 'image/svg+xml' - ] - ]; + /** + * Fixes an invalid MIME type guess for the given file + * + * @param string $file + * @param string $mime + * @param string $extension + * @return string|null + */ + public static function fix(string $file, string $mime = null, string $extension = null) + { + // fixing map + $map = [ + 'text/html' => [ + 'svg' => ['Kirby\Filesystem\Mime', 'fromSvg'], + ], + 'text/plain' => [ + 'css' => 'text/css', + 'json' => 'application/json', + 'mjs' => 'text/javascript', + 'svg' => ['Kirby\Filesystem\Mime', 'fromSvg'], + ], + 'text/x-asm' => [ + 'css' => 'text/css' + ], + 'text/x-java' => [ + 'mjs' => 'text/javascript', + ], + 'image/svg' => [ + 'svg' => 'image/svg+xml' + ], + 'application/octet-stream' => [ + 'mjs' => 'text/javascript' + ] + ]; - if ($mode = ($map[$mime][$extension] ?? null)) { - if (is_callable($mode) === true) { - return $mode($file, $mime, $extension); - } + if ($mode = ($map[$mime][$extension] ?? null)) { + if (is_callable($mode) === true) { + return $mode($file, $mime, $extension); + } - if (is_string($mode) === true) { - return $mode; - } - } + if (is_string($mode) === true) { + return $mode; + } + } - return $mime; - } + return $mime; + } - /** - * Guesses a MIME type by extension - * - * @param string $extension - * @return string|null - */ - public static function fromExtension(string $extension): ?string - { - $mime = static::$types[$extension] ?? null; - return is_array($mime) === true ? array_shift($mime) : $mime; - } + /** + * Guesses a MIME type by extension + * + * @param string $extension + * @return string|null + */ + public static function fromExtension(string $extension): ?string + { + $mime = static::$types[$extension] ?? null; + return is_array($mime) === true ? array_shift($mime) : $mime; + } - /** - * Returns the MIME type of a file - * - * @param string $file - * @return string|false - */ - public static function fromFileInfo(string $file) - { - if (function_exists('finfo_file') === true && file_exists($file) === true) { - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime = finfo_file($finfo, $file); - finfo_close($finfo); - return $mime; - } + /** + * Returns the MIME type of a file + * + * @param string $file + * @return string|false + */ + public static function fromFileInfo(string $file) + { + if (function_exists('finfo_file') === true && file_exists($file) === true) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $file); + finfo_close($finfo); + return $mime; + } - return false; - } + return false; + } - /** - * Returns the MIME type of a file - * - * @param string $file - * @return string|false - */ - public static function fromMimeContentType(string $file) - { - if (function_exists('mime_content_type') === true && file_exists($file) === true) { - return mime_content_type($file); - } + /** + * Returns the MIME type of a file + * + * @param string $file + * @return string|false + */ + public static function fromMimeContentType(string $file) + { + if (function_exists('mime_content_type') === true && file_exists($file) === true) { + return mime_content_type($file); + } - return false; - } + return false; + } - /** - * Tries to detect a valid SVG and returns the MIME type accordingly - * - * @param string $file - * @return string|false - */ - public static function fromSvg(string $file) - { - if (file_exists($file) === true) { - libxml_use_internal_errors(true); + /** + * Tries to detect a valid SVG and returns the MIME type accordingly + * + * @param string $file + * @return string|false + */ + public static function fromSvg(string $file) + { + if (file_exists($file) === true) { + libxml_use_internal_errors(true); - $svg = new SimpleXMLElement(file_get_contents($file)); + $svg = new SimpleXMLElement(file_get_contents($file)); - if ($svg !== false && $svg->getName() === 'svg') { - return 'image/svg+xml'; - } - } + if ($svg !== false && $svg->getName() === 'svg') { + return 'image/svg+xml'; + } + } - return false; - } + return false; + } - /** - * Tests if a given MIME type is matched by an `Accept` header - * pattern; returns true if the MIME type is contained at all - * - * @param string $mime - * @param string $pattern - * @return bool - */ - public static function isAccepted(string $mime, string $pattern): bool - { - $accepted = Str::accepted($pattern); + /** + * Tests if a given MIME type is matched by an `Accept` header + * pattern; returns true if the MIME type is contained at all + * + * @param string $mime + * @param string $pattern + * @return bool + */ + public static function isAccepted(string $mime, string $pattern): bool + { + $accepted = Str::accepted($pattern); - foreach ($accepted as $m) { - if (static::matches($mime, $m['value']) === true) { - return true; - } - } + foreach ($accepted as $m) { + if (static::matches($mime, $m['value']) === true) { + return true; + } + } - return false; - } + return false; + } - /** - * Tests if a MIME wildcard pattern from an `Accept` header - * matches a given type - * @since 3.3.0 - * - * @param string $test - * @param string $wildcard - * @return bool - */ - public static function matches(string $test, string $wildcard): bool - { - return fnmatch($wildcard, $test, FNM_PATHNAME) === true; - } + /** + * Tests if a MIME wildcard pattern from an `Accept` header + * matches a given type + * @since 3.3.0 + * + * @param string $test + * @param string $wildcard + * @return bool + */ + public static function matches(string $test, string $wildcard): bool + { + return fnmatch($wildcard, $test, FNM_PATHNAME) === true; + } - /** - * Returns the extension for a given MIME type - * - * @param string|null $mime - * @return string|false - */ - public static function toExtension(string $mime = null) - { - foreach (static::$types as $key => $value) { - if (is_array($value) === true && in_array($mime, $value) === true) { - return $key; - } + /** + * Returns the extension for a given MIME type + * + * @param string|null $mime + * @return string|false + */ + public static function toExtension(string $mime = null) + { + foreach (static::$types as $key => $value) { + if (is_array($value) === true && in_array($mime, $value) === true) { + return $key; + } - if ($value === $mime) { - return $key; - } - } + if ($value === $mime) { + return $key; + } + } - return false; - } + return false; + } - /** - * Returns all available extensions for a given MIME type - * - * @param string|null $mime - * @return array - */ - public static function toExtensions(string $mime = null): array - { - $extensions = []; + /** + * Returns all available extensions for a given MIME type + * + * @param string|null $mime + * @return array + */ + public static function toExtensions(string $mime = null): array + { + $extensions = []; - foreach (static::$types as $key => $value) { - if (is_array($value) === true && in_array($mime, $value) === true) { - $extensions[] = $key; - continue; - } + foreach (static::$types as $key => $value) { + if (is_array($value) === true && in_array($mime, $value) === true) { + $extensions[] = $key; + continue; + } - if ($value === $mime) { - $extensions[] = $key; - } - } + if ($value === $mime) { + $extensions[] = $key; + } + } - return $extensions; - } + return $extensions; + } - /** - * Returns the MIME type of a file - * - * @param string $file - * @param string $extension - * @return string|false - */ - public static function type(string $file, string $extension = null) - { - // use the standard finfo extension - $mime = static::fromFileInfo($file); + /** + * Returns the MIME type of a file + * + * @param string $file + * @param string $extension + * @return string|false + */ + public static function type(string $file, string $extension = null) + { + // use the standard finfo extension + $mime = static::fromFileInfo($file); - // use the mime_content_type function - if ($mime === false) { - $mime = static::fromMimeContentType($file); - } + // use the mime_content_type function + if ($mime === false) { + $mime = static::fromMimeContentType($file); + } - // get the extension or extract it from the filename - $extension ??= F::extension($file); + // get the extension or extract it from the filename + $extension ??= F::extension($file); - // try to guess the mime type at least - if ($mime === false) { - $mime = static::fromExtension($extension); - } + // try to guess the mime type at least + if ($mime === false) { + $mime = static::fromExtension($extension); + } - // fix broken mime detection - return static::fix($file, $mime, $extension); - } + // fix broken mime detection + return static::fix($file, $mime, $extension); + } - /** - * Returns all detectable MIME types - * - * @return array - */ - public static function types(): array - { - return static::$types; - } + /** + * Returns all detectable MIME types + * + * @return array + */ + public static function types(): array + { + return static::$types; + } } diff --git a/kirby/src/Form/Field.php b/kirby/src/Form/Field.php index 6d9e473..34f4a62 100644 --- a/kirby/src/Form/Field.php +++ b/kirby/src/Form/Field.php @@ -22,486 +22,486 @@ use Kirby\Toolkit\V; */ class Field extends Component { - /** - * An array of all found errors - * - * @var array|null - */ - protected $errors; + /** + * An array of all found errors + * + * @var array|null + */ + protected $errors; - /** - * Parent collection with all fields of the current form - * - * @var \Kirby\Form\Fields|null - */ - protected $formFields; + /** + * Parent collection with all fields of the current form + * + * @var \Kirby\Form\Fields|null + */ + protected $formFields; - /** - * Registry for all component mixins - * - * @var array - */ - public static $mixins = []; + /** + * Registry for all component mixins + * + * @var array + */ + public static $mixins = []; - /** - * Registry for all component types - * - * @var array - */ - public static $types = []; + /** + * Registry for all component types + * + * @var array + */ + public static $types = []; - /** - * Field constructor - * - * @param string $type - * @param array $attrs - * @param \Kirby\Form\Fields|null $formFields - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function __construct(string $type, array $attrs = [], ?Fields $formFields = null) - { - if (isset(static::$types[$type]) === false) { - throw new InvalidArgumentException('The field type "' . $type . '" does not exist'); - } + /** + * Field constructor + * + * @param string $type + * @param array $attrs + * @param \Kirby\Form\Fields|null $formFields + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function __construct(string $type, array $attrs = [], ?Fields $formFields = null) + { + if (isset(static::$types[$type]) === false) { + throw new InvalidArgumentException('The field type "' . $type . '" does not exist'); + } - if (isset($attrs['model']) === false) { - throw new InvalidArgumentException('Field requires a model'); - } + if (isset($attrs['model']) === false) { + throw new InvalidArgumentException('Field requires a model'); + } - $this->formFields = $formFields; + $this->formFields = $formFields; - // use the type as fallback for the name - $attrs['name'] ??= $type; - $attrs['type'] = $type; + // use the type as fallback for the name + $attrs['name'] ??= $type; + $attrs['type'] = $type; - parent::__construct($type, $attrs); - } + parent::__construct($type, $attrs); + } - /** - * Returns field api call - * - * @return mixed - */ - public function api() - { - if ( - isset($this->options['api']) === true && - is_a($this->options['api'], 'Closure') === true - ) { - return $this->options['api']->call($this); - } - } + /** + * Returns field api call + * + * @return mixed + */ + public function api() + { + if ( + isset($this->options['api']) === true && + is_a($this->options['api'], 'Closure') === true + ) { + return $this->options['api']->call($this); + } + } - /** - * Returns field data - * - * @param bool $default - * @return mixed - */ - public function data(bool $default = false) - { - $save = $this->options['save'] ?? true; + /** + * Returns field data + * + * @param bool $default + * @return mixed + */ + public function data(bool $default = false) + { + $save = $this->options['save'] ?? true; - if ($default === true && $this->isEmpty($this->value)) { - $value = $this->default(); - } else { - $value = $this->value; - } + if ($default === true && $this->isEmpty($this->value)) { + $value = $this->default(); + } else { + $value = $this->value; + } - if ($save === false) { - return null; - } + if ($save === false) { + return null; + } - if (is_a($save, 'Closure') === true) { - return $save->call($this, $value); - } + if (is_a($save, 'Closure') === true) { + return $save->call($this, $value); + } - return $value; - } + return $value; + } - /** - * Default props and computed of the field - * - * @return array - */ - public static function defaults(): array - { - return [ - 'props' => [ - /** - * Optional text that will be shown after the input - */ - 'after' => function ($after = null) { - return I18n::translate($after, $after); - }, - /** - * Sets the focus on this field when the form loads. Only the first field with this label gets - */ - 'autofocus' => function (bool $autofocus = null): bool { - return $autofocus ?? false; - }, - /** - * Optional text that will be shown before the input - */ - 'before' => function ($before = null) { - return I18n::translate($before, $before); - }, - /** - * Default value for the field, which will be used when a page/file/user is created - */ - 'default' => function ($default = null) { - return $default; - }, - /** - * If `true`, the field is no longer editable and will not be saved - */ - 'disabled' => function (bool $disabled = null): bool { - return $disabled ?? false; - }, - /** - * Optional help text below the field - */ - 'help' => function ($help = null) { - return I18n::translate($help, $help); - }, - /** - * Optional icon that will be shown at the end of the field - */ - 'icon' => function (string $icon = null) { - return $icon; - }, - /** - * The field label can be set as string or associative array with translations - */ - 'label' => function ($label = null) { - return I18n::translate($label, $label); - }, - /** - * Optional placeholder value that will be shown when the field is empty - */ - 'placeholder' => function ($placeholder = null) { - return I18n::translate($placeholder, $placeholder); - }, - /** - * If `true`, the field has to be filled in correctly to be saved. - */ - 'required' => function (bool $required = null): bool { - return $required ?? false; - }, - /** - * If `false`, the field will be disabled in non-default languages and cannot be translated. This is only relevant in multi-language setups. - */ - 'translate' => function (bool $translate = true): bool { - return $translate; - }, - /** - * Conditions when the field will be shown (since 3.1.0) - */ - 'when' => function ($when = null) { - return $when; - }, - /** - * The width of the field in the field grid. Available widths: `1/1`, `1/2`, `1/3`, `1/4`, `2/3`, `3/4` - */ - 'width' => function (string $width = '1/1') { - return $width; - }, - 'value' => function ($value = null) { - return $value; - } - ], - 'computed' => [ - 'after' => function () { - /** @var \Kirby\Form\Field $this */ - if ($this->after !== null) { - return $this->model()->toString($this->after); - } - }, - 'before' => function () { - /** @var \Kirby\Form\Field $this */ - if ($this->before !== null) { - return $this->model()->toString($this->before); - } - }, - 'default' => function () { - /** @var \Kirby\Form\Field $this */ - if ($this->default === null) { - return; - } + /** + * Default props and computed of the field + * + * @return array + */ + public static function defaults(): array + { + return [ + 'props' => [ + /** + * Optional text that will be shown after the input + */ + 'after' => function ($after = null) { + return I18n::translate($after, $after); + }, + /** + * Sets the focus on this field when the form loads. Only the first field with this label gets + */ + 'autofocus' => function (bool $autofocus = null): bool { + return $autofocus ?? false; + }, + /** + * Optional text that will be shown before the input + */ + 'before' => function ($before = null) { + return I18n::translate($before, $before); + }, + /** + * Default value for the field, which will be used when a page/file/user is created + */ + 'default' => function ($default = null) { + return $default; + }, + /** + * If `true`, the field is no longer editable and will not be saved + */ + 'disabled' => function (bool $disabled = null): bool { + return $disabled ?? false; + }, + /** + * Optional help text below the field + */ + 'help' => function ($help = null) { + return I18n::translate($help, $help); + }, + /** + * Optional icon that will be shown at the end of the field + */ + 'icon' => function (string $icon = null) { + return $icon; + }, + /** + * The field label can be set as string or associative array with translations + */ + 'label' => function ($label = null) { + return I18n::translate($label, $label); + }, + /** + * Optional placeholder value that will be shown when the field is empty + */ + 'placeholder' => function ($placeholder = null) { + return I18n::translate($placeholder, $placeholder); + }, + /** + * If `true`, the field has to be filled in correctly to be saved. + */ + 'required' => function (bool $required = null): bool { + return $required ?? false; + }, + /** + * If `false`, the field will be disabled in non-default languages and cannot be translated. This is only relevant in multi-language setups. + */ + 'translate' => function (bool $translate = true): bool { + return $translate; + }, + /** + * Conditions when the field will be shown (since 3.1.0) + */ + 'when' => function ($when = null) { + return $when; + }, + /** + * The width of the field in the field grid. Available widths: `1/1`, `1/2`, `1/3`, `1/4`, `2/3`, `3/4` + */ + 'width' => function (string $width = '1/1') { + return $width; + }, + 'value' => function ($value = null) { + return $value; + } + ], + 'computed' => [ + 'after' => function () { + /** @var \Kirby\Form\Field $this */ + if ($this->after !== null) { + return $this->model()->toString($this->after); + } + }, + 'before' => function () { + /** @var \Kirby\Form\Field $this */ + if ($this->before !== null) { + return $this->model()->toString($this->before); + } + }, + 'default' => function () { + /** @var \Kirby\Form\Field $this */ + if ($this->default === null) { + return; + } - if (is_string($this->default) === false) { - return $this->default; - } + if (is_string($this->default) === false) { + return $this->default; + } - return $this->model()->toString($this->default); - }, - 'help' => function () { - /** @var \Kirby\Form\Field $this */ - if ($this->help) { - $help = $this->model()->toSafeString($this->help); - $help = $this->kirby()->kirbytext($help); - return $help; - } - }, - 'label' => function () { - /** @var \Kirby\Form\Field $this */ - if ($this->label !== null) { - return $this->model()->toString($this->label); - } - }, - 'placeholder' => function () { - /** @var \Kirby\Form\Field $this */ - if ($this->placeholder !== null) { - return $this->model()->toString($this->placeholder); - } - } - ] - ]; - } + return $this->model()->toString($this->default); + }, + 'help' => function () { + /** @var \Kirby\Form\Field $this */ + if ($this->help) { + $help = $this->model()->toSafeString($this->help); + $help = $this->kirby()->kirbytext($help); + return $help; + } + }, + 'label' => function () { + /** @var \Kirby\Form\Field $this */ + if ($this->label !== null) { + return $this->model()->toString($this->label); + } + }, + 'placeholder' => function () { + /** @var \Kirby\Form\Field $this */ + if ($this->placeholder !== null) { + return $this->model()->toString($this->placeholder); + } + } + ] + ]; + } - /** - * Creates a new field instance - * - * @param string $type - * @param array $attrs - * @param Fields|null $formFields - * @return static - */ - public static function factory(string $type, array $attrs = [], ?Fields $formFields = null) - { - $field = static::$types[$type] ?? null; + /** + * Creates a new field instance + * + * @param string $type + * @param array $attrs + * @param Fields|null $formFields + * @return static + */ + public static function factory(string $type, array $attrs = [], ?Fields $formFields = null) + { + $field = static::$types[$type] ?? null; - if (is_string($field) && class_exists($field) === true) { - $attrs['siblings'] = $formFields; - return new $field($attrs); - } + if (is_string($field) && class_exists($field) === true) { + $attrs['siblings'] = $formFields; + return new $field($attrs); + } - return new static($type, $attrs, $formFields); - } + return new static($type, $attrs, $formFields); + } - /** - * Parent collection with all fields of the current form - * - * @return \Kirby\Form\Fields|null - */ - public function formFields(): ?Fields - { - return $this->formFields; - } + /** + * Parent collection with all fields of the current form + * + * @return \Kirby\Form\Fields|null + */ + public function formFields(): ?Fields + { + return $this->formFields; + } - /** - * Validates when run for the first time and returns any errors - * - * @return array - */ - public function errors(): array - { - if ($this->errors === null) { - $this->validate(); - } + /** + * Validates when run for the first time and returns any errors + * + * @return array + */ + public function errors(): array + { + if ($this->errors === null) { + $this->validate(); + } - return $this->errors; - } + return $this->errors; + } - /** - * Checks if the field is empty - * - * @param mixed ...$args - * @return bool - */ - public function isEmpty(...$args): bool - { - if (count($args) === 0) { - $value = $this->value(); - } else { - $value = $args[0]; - } + /** + * Checks if the field is empty + * + * @param mixed ...$args + * @return bool + */ + public function isEmpty(...$args): bool + { + if (count($args) === 0) { + $value = $this->value(); + } else { + $value = $args[0]; + } - if (isset($this->options['isEmpty']) === true) { - return $this->options['isEmpty']->call($this, $value); - } + if (isset($this->options['isEmpty']) === true) { + return $this->options['isEmpty']->call($this, $value); + } - return in_array($value, [null, '', []], true); - } + return in_array($value, [null, '', []], true); + } - /** - * Checks if the field is invalid - * - * @return bool - */ - public function isInvalid(): bool - { - return empty($this->errors()) === false; - } + /** + * Checks if the field is invalid + * + * @return bool + */ + public function isInvalid(): bool + { + return empty($this->errors()) === false; + } - /** - * Checks if the field is required - * - * @return bool - */ - public function isRequired(): bool - { - return $this->required ?? false; - } + /** + * Checks if the field is required + * + * @return bool + */ + public function isRequired(): bool + { + return $this->required ?? false; + } - /** - * Checks if the field is valid - * - * @return bool - */ - public function isValid(): bool - { - return empty($this->errors()) === true; - } + /** + * Checks if the field is valid + * + * @return bool + */ + public function isValid(): bool + { + return empty($this->errors()) === true; + } - /** - * Returns the Kirby instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return $this->model()->kirby(); - } + /** + * Returns the Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model()->kirby(); + } - /** - * Returns the parent model - * - * @return mixed - */ - public function model() - { - return $this->model; - } + /** + * Returns the parent model + * + * @return mixed + */ + public function model() + { + return $this->model; + } - /** - * Checks if the field needs a value before being saved; - * this is the case if all of the following requirements are met: - * - The field is saveable - * - The field is required - * - The field is currently empty - * - The field is not currently inactive because of a `when` rule - * - * @return bool - */ - protected function needsValue(): bool - { - // check simple conditions first - if ($this->save() === false || $this->isRequired() === false || $this->isEmpty() === false) { - return false; - } + /** + * Checks if the field needs a value before being saved; + * this is the case if all of the following requirements are met: + * - The field is saveable + * - The field is required + * - The field is currently empty + * - The field is not currently inactive because of a `when` rule + * + * @return bool + */ + protected function needsValue(): bool + { + // check simple conditions first + if ($this->save() === false || $this->isRequired() === false || $this->isEmpty() === false) { + return false; + } - // check the data of the relevant fields if there is a `when` option - if (empty($this->when) === false && is_array($this->when) === true) { - $formFields = $this->formFields(); + // check the data of the relevant fields if there is a `when` option + if (empty($this->when) === false && is_array($this->when) === true) { + $formFields = $this->formFields(); - if ($formFields !== null) { - foreach ($this->when as $field => $value) { - $field = $formFields->get($field); - $inputValue = $field !== null ? $field->value() : ''; + if ($formFields !== null) { + foreach ($this->when as $field => $value) { + $field = $formFields->get($field); + $inputValue = $field !== null ? $field->value() : ''; - // if the input data doesn't match the requested `when` value, - // that means that this field is not required and can be saved - // (*all* `when` conditions must be met for this field to be required) - if ($inputValue !== $value) { - return false; - } - } - } - } + // if the input data doesn't match the requested `when` value, + // that means that this field is not required and can be saved + // (*all* `when` conditions must be met for this field to be required) + if ($inputValue !== $value) { + return false; + } + } + } + } - // either there was no `when` condition or all conditions matched - return true; - } + // either there was no `when` condition or all conditions matched + return true; + } - /** - * Checks if the field is saveable - * - * @return bool - */ - public function save(): bool - { - return ($this->options['save'] ?? true) !== false; - } + /** + * Checks if the field is saveable + * + * @return bool + */ + public function save(): bool + { + return ($this->options['save'] ?? true) !== false; + } - /** - * Converts the field to a plain array - * - * @return array - */ - public function toArray(): array - { - $array = parent::toArray(); + /** + * Converts the field to a plain array + * + * @return array + */ + public function toArray(): array + { + $array = parent::toArray(); - unset($array['model']); + unset($array['model']); - $array['saveable'] = $this->save(); - $array['signature'] = md5(json_encode($array)); + $array['saveable'] = $this->save(); + $array['signature'] = md5(json_encode($array)); - ksort($array); + ksort($array); - return array_filter( - $array, - fn ($item) => $item !== null && is_object($item) === false - ); - } + return array_filter( + $array, + fn ($item) => $item !== null && is_object($item) === false + ); + } - /** - * Runs the validations defined for the field - * - * @return void - */ - protected function validate(): void - { - $validations = $this->options['validations'] ?? []; - $this->errors = []; + /** + * Runs the validations defined for the field + * + * @return void + */ + protected function validate(): void + { + $validations = $this->options['validations'] ?? []; + $this->errors = []; - // validate required values - if ($this->needsValue() === true) { - $this->errors['required'] = I18n::translate('error.validation.required'); - } + // validate required values + if ($this->needsValue() === true) { + $this->errors['required'] = I18n::translate('error.validation.required'); + } - foreach ($validations as $key => $validation) { - if (is_int($key) === true) { - // predefined validation - try { - Validations::$validation($this, $this->value()); - } catch (Exception $e) { - $this->errors[$validation] = $e->getMessage(); - } - continue; - } + foreach ($validations as $key => $validation) { + if (is_int($key) === true) { + // predefined validation + try { + Validations::$validation($this, $this->value()); + } catch (Exception $e) { + $this->errors[$validation] = $e->getMessage(); + } + continue; + } - if (is_a($validation, 'Closure') === true) { - try { - $validation->call($this, $this->value()); - } catch (Exception $e) { - $this->errors[$key] = $e->getMessage(); - } - } - } + if (is_a($validation, 'Closure') === true) { + try { + $validation->call($this, $this->value()); + } catch (Exception $e) { + $this->errors[$key] = $e->getMessage(); + } + } + } - if ( - empty($this->validate) === false && - ($this->isEmpty() === false || $this->isRequired() === true) - ) { - $rules = A::wrap($this->validate); - $errors = V::errors($this->value(), $rules); + if ( + empty($this->validate) === false && + ($this->isEmpty() === false || $this->isRequired() === true) + ) { + $rules = A::wrap($this->validate); + $errors = V::errors($this->value(), $rules); - if (empty($errors) === false) { - $this->errors = array_merge($this->errors, $errors); - } - } - } + if (empty($errors) === false) { + $this->errors = array_merge($this->errors, $errors); + } + } + } - /** - * Returns the value of the field if saveable - * otherwise it returns null - * - * @return mixed - */ - public function value() - { - return $this->save() ? $this->value : null; - } + /** + * Returns the value of the field if saveable + * otherwise it returns null + * + * @return mixed + */ + public function value() + { + return $this->save() ? $this->value : null; + } } diff --git a/kirby/src/Form/Field/BlocksField.php b/kirby/src/Form/Field/BlocksField.php index 7afa288..6479b8a 100644 --- a/kirby/src/Form/Field/BlocksField.php +++ b/kirby/src/Form/Field/BlocksField.php @@ -2,6 +2,7 @@ namespace Kirby\Form\Field; +use Kirby\Cms\App; use Kirby\Cms\Block; use Kirby\Cms\Blocks as BlocksCollection; use Kirby\Cms\Fieldsets; @@ -12,270 +13,273 @@ use Kirby\Form\Form; use Kirby\Form\Mixin\EmptyState; use Kirby\Form\Mixin\Max; use Kirby\Form\Mixin\Min; +use Kirby\Toolkit\Str; use Throwable; class BlocksField extends FieldClass { - use EmptyState; - use Max; - use Min; + use EmptyState; + use Max; + use Min; - protected $blocks; - protected $fieldsets; - protected $group; - protected $pretty; - protected $value = []; + protected $blocks; + protected $fieldsets; + protected $group; + protected $pretty; + protected $value = []; - public function __construct(array $params = []) - { - $this->setFieldsets($params['fieldsets'] ?? null, $params['model'] ?? site()); + public function __construct(array $params = []) + { + $this->setFieldsets($params['fieldsets'] ?? null, $params['model'] ?? App::instance()->site()); - parent::__construct($params); + parent::__construct($params); - $this->setEmpty($params['empty'] ?? null); - $this->setGroup($params['group'] ?? 'blocks'); - $this->setMax($params['max'] ?? null); - $this->setMin($params['min'] ?? null); - $this->setPretty($params['pretty'] ?? false); - } + $this->setEmpty($params['empty'] ?? null); + $this->setGroup($params['group'] ?? 'blocks'); + $this->setMax($params['max'] ?? null); + $this->setMin($params['min'] ?? null); + $this->setPretty($params['pretty'] ?? false); + } - public function blocksToValues($blocks, $to = 'values'): array - { - $result = []; - $fields = []; + public function blocksToValues($blocks, $to = 'values'): array + { + $result = []; + $fields = []; - foreach ($blocks as $block) { - try { - $type = $block['type']; + foreach ($blocks as $block) { + try { + $type = $block['type']; - // get and cache fields at the same time - $fields[$type] ??= $this->fields($block['type']); + // get and cache fields at the same time + $fields[$type] ??= $this->fields($block['type']); - // overwrite the block content with form values - $block['content'] = $this->form($fields[$type], $block['content'])->$to(); + // overwrite the block content with form values + $block['content'] = $this->form($fields[$type], $block['content'])->$to(); - $result[] = $block; - } catch (Throwable $e) { - $result[] = $block; + $result[] = $block; + } catch (Throwable $e) { + $result[] = $block; - // skip invalid blocks - continue; - } - } + // skip invalid blocks + continue; + } + } - return $result; - } + return $result; + } - public function fields(string $type) - { - return $this->fieldset($type)->fields(); - } + public function fields(string $type) + { + return $this->fieldset($type)->fields(); + } - public function fieldset(string $type) - { - if ($fieldset = $this->fieldsets->find($type)) { - return $fieldset; - } + public function fieldset(string $type) + { + if ($fieldset = $this->fieldsets->find($type)) { + return $fieldset; + } - throw new NotFoundException('The fieldset ' . $type . ' could not be found'); - } + throw new NotFoundException('The fieldset ' . $type . ' could not be found'); + } - public function fieldsets() - { - return $this->fieldsets; - } + public function fieldsets() + { + return $this->fieldsets; + } - public function fieldsetGroups(): ?array - { - $fieldsetGroups = $this->fieldsets()->groups(); - return empty($fieldsetGroups) === true ? null : $fieldsetGroups; - } + public function fieldsetGroups(): ?array + { + $fieldsetGroups = $this->fieldsets()->groups(); + return empty($fieldsetGroups) === true ? null : $fieldsetGroups; + } - public function fill($value = null) - { - $value = BlocksCollection::parse($value); - $blocks = BlocksCollection::factory($value); - $this->value = $this->blocksToValues($blocks->toArray()); - } + public function fill($value = null) + { + $value = BlocksCollection::parse($value); + $blocks = BlocksCollection::factory($value); + $this->value = $this->blocksToValues($blocks->toArray()); + } - public function form(array $fields, array $input = []) - { - return new Form([ - 'fields' => $fields, - 'model' => $this->model, - 'strict' => true, - 'values' => $input, - ]); - } + public function form(array $fields, array $input = []) + { + return new Form([ + 'fields' => $fields, + 'model' => $this->model, + 'strict' => true, + 'values' => $input, + ]); + } - public function isEmpty(): bool - { - return count($this->value()) === 0; - } + public function isEmpty(): bool + { + return count($this->value()) === 0; + } - public function group(): string - { - return $this->group; - } + public function group(): string + { + return $this->group; + } - public function pretty(): bool - { - return $this->pretty; - } + public function pretty(): bool + { + return $this->pretty; + } - public function props(): array - { - return [ - 'empty' => $this->empty(), - 'fieldsets' => $this->fieldsets()->toArray(), - 'fieldsetGroups' => $this->fieldsetGroups(), - 'group' => $this->group(), - 'max' => $this->max(), - 'min' => $this->min(), - ] + parent::props(); - } + public function props(): array + { + return [ + 'empty' => $this->empty(), + 'fieldsets' => $this->fieldsets()->toArray(), + 'fieldsetGroups' => $this->fieldsetGroups(), + 'group' => $this->group(), + 'max' => $this->max(), + 'min' => $this->min(), + ] + parent::props(); + } - public function routes(): array - { - $field = $this; + public function routes(): array + { + $field = $this; - return [ - [ - 'pattern' => 'uuid', - 'action' => fn () => ['uuid' => uuid()] - ], - [ - 'pattern' => 'paste', - 'method' => 'POST', - 'action' => function () use ($field) { - $value = BlocksCollection::parse(get('html')); - $blocks = BlocksCollection::factory($value); - return $field->blocksToValues($blocks->toArray()); - } - ], - [ - 'pattern' => 'fieldsets/(:any)', - 'method' => 'GET', - 'action' => function ($fieldsetType) use ($field) { - $fields = $field->fields($fieldsetType); - $defaults = $field->form($fields, [])->data(true); - $content = $field->form($fields, $defaults)->values(); + return [ + [ + 'pattern' => 'uuid', + 'action' => fn () => ['uuid' => Str::uuid()] + ], + [ + 'pattern' => 'paste', + 'method' => 'POST', + 'action' => function () use ($field) { + $request = App::instance()->request(); - return Block::factory([ - 'content' => $content, - 'type' => $fieldsetType - ])->toArray(); - } - ], - [ - 'pattern' => 'fieldsets/(:any)/fields/(:any)/(:all?)', - 'method' => 'ALL', - 'action' => function (string $fieldsetType, string $fieldName, string $path = null) use ($field) { - $fields = $field->fields($fieldsetType); - $field = $field->form($fields)->field($fieldName); + $value = BlocksCollection::parse($request->get('html')); + $blocks = BlocksCollection::factory($value); + return $field->blocksToValues($blocks->toArray()); + } + ], + [ + 'pattern' => 'fieldsets/(:any)', + 'method' => 'GET', + 'action' => function ($fieldsetType) use ($field) { + $fields = $field->fields($fieldsetType); + $defaults = $field->form($fields, [])->data(true); + $content = $field->form($fields, $defaults)->values(); - $fieldApi = $this->clone([ - 'routes' => $field->api(), - 'data' => array_merge($this->data(), ['field' => $field]) - ]); + return Block::factory([ + 'content' => $content, + 'type' => $fieldsetType + ])->toArray(); + } + ], + [ + 'pattern' => 'fieldsets/(:any)/fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $fieldsetType, string $fieldName, string $path = null) use ($field) { + $fields = $field->fields($fieldsetType); + $field = $field->form($fields)->field($fieldName); - return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); - } - ], - ]; - } + $fieldApi = $this->clone([ + 'routes' => $field->api(), + 'data' => array_merge($this->data(), ['field' => $field]) + ]); - public function store($value) - { - $blocks = $this->blocksToValues((array)$value, 'content'); + return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); + } + ], + ]; + } - // returns empty string to avoid storing empty array as string `[]` - // and to consistency work with `$field->isEmpty()` - if (empty($blocks) === true) { - return ''; - } + public function store($value) + { + $blocks = $this->blocksToValues((array)$value, 'content'); - return $this->valueToJson($blocks, $this->pretty()); - } + // returns empty string to avoid storing empty array as string `[]` + // and to consistency work with `$field->isEmpty()` + if (empty($blocks) === true) { + return ''; + } - protected function setFieldsets($fieldsets, $model) - { - if (is_string($fieldsets) === true) { - $fieldsets = []; - } + return $this->valueToJson($blocks, $this->pretty()); + } - $this->fieldsets = Fieldsets::factory($fieldsets, [ - 'parent' => $model - ]); - } + protected function setFieldsets($fieldsets, $model) + { + if (is_string($fieldsets) === true) { + $fieldsets = []; + } - protected function setGroup(string $group = null) - { - $this->group = $group; - } + $this->fieldsets = Fieldsets::factory($fieldsets, [ + 'parent' => $model + ]); + } - protected function setPretty(bool $pretty = false) - { - $this->pretty = $pretty; - } + protected function setGroup(string $group = null) + { + $this->group = $group; + } - public function validations(): array - { - return [ - 'blocks' => function ($value) { - if ($this->min && count($value) < $this->min) { - throw new InvalidArgumentException([ - 'key' => 'blocks.min.' . ($this->min === 1 ? 'singular' : 'plural'), - 'data' => [ - 'min' => $this->min - ] - ]); - } + protected function setPretty(bool $pretty = false) + { + $this->pretty = $pretty; + } - if ($this->max && count($value) > $this->max) { - throw new InvalidArgumentException([ - 'key' => 'blocks.max.' . ($this->max === 1 ? 'singular' : 'plural'), - 'data' => [ - 'max' => $this->max - ] - ]); - } + public function validations(): array + { + return [ + 'blocks' => function ($value) { + if ($this->min && count($value) < $this->min) { + throw new InvalidArgumentException([ + 'key' => 'blocks.min.' . ($this->min === 1 ? 'singular' : 'plural'), + 'data' => [ + 'min' => $this->min + ] + ]); + } - $fields = []; - $index = 0; + if ($this->max && count($value) > $this->max) { + throw new InvalidArgumentException([ + 'key' => 'blocks.max.' . ($this->max === 1 ? 'singular' : 'plural'), + 'data' => [ + 'max' => $this->max + ] + ]); + } - foreach ($value as $block) { - $index++; - $blockType = $block['type']; + $fields = []; + $index = 0; - try { - $blockFields = $fields[$blockType] ?? $this->fields($blockType) ?? []; - } catch (Throwable $e) { - // skip invalid blocks - continue; - } + foreach ($value as $block) { + $index++; + $blockType = $block['type']; - // store the fields for the next round - $fields[$blockType] = $blockFields; + try { + $blockFields = $fields[$blockType] ?? $this->fields($blockType) ?? []; + } catch (Throwable $e) { + // skip invalid blocks + continue; + } - // overwrite the content with the serialized form - foreach ($this->form($blockFields, $block['content'])->fields() as $field) { - $errors = $field->errors(); + // store the fields for the next round + $fields[$blockType] = $blockFields; - // rough first validation - if (empty($errors) === false) { - throw new InvalidArgumentException([ - 'key' => 'blocks.validation', - 'data' => [ - 'index' => $index, - ] - ]); - } - } - } + // overwrite the content with the serialized form + foreach ($this->form($blockFields, $block['content'])->fields() as $field) { + $errors = $field->errors(); - return true; - } - ]; - } + // rough first validation + if (empty($errors) === false) { + throw new InvalidArgumentException([ + 'key' => 'blocks.validation', + 'data' => [ + 'index' => $index, + ] + ]); + } + } + } + + return true; + } + ]; + } } diff --git a/kirby/src/Form/Field/LayoutField.php b/kirby/src/Form/Field/LayoutField.php index 02222ba..640d095 100644 --- a/kirby/src/Form/Field/LayoutField.php +++ b/kirby/src/Form/Field/LayoutField.php @@ -2,6 +2,7 @@ namespace Kirby\Form\Field; +use Kirby\Cms\App; use Kirby\Cms\Blueprint; use Kirby\Cms\Fieldset; use Kirby\Cms\Layout; @@ -13,220 +14,222 @@ use Throwable; class LayoutField extends BlocksField { - protected $layouts; - protected $settings; + protected $layouts; + protected $settings; - public function __construct(array $params) - { - $this->setModel($params['model'] ?? site()); - $this->setLayouts($params['layouts'] ?? ['1/1']); - $this->setSettings($params['settings'] ?? null); + public function __construct(array $params) + { + $this->setModel($params['model'] ?? App::instance()->site()); + $this->setLayouts($params['layouts'] ?? ['1/1']); + $this->setSettings($params['settings'] ?? null); - parent::__construct($params); - } + parent::__construct($params); + } - public function fill($value = null) - { - $value = $this->valueFromJson($value); - $layouts = Layouts::factory($value, ['parent' => $this->model])->toArray(); + public function fill($value = null) + { + $value = $this->valueFromJson($value); + $layouts = Layouts::factory($value, ['parent' => $this->model])->toArray(); - foreach ($layouts as $layoutIndex => $layout) { - if ($this->settings !== null) { - $layouts[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->values(); - } + foreach ($layouts as $layoutIndex => $layout) { + if ($this->settings !== null) { + $layouts[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->values(); + } - foreach ($layout['columns'] as $columnIndex => $column) { - $layouts[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks']); - } - } + foreach ($layout['columns'] as $columnIndex => $column) { + $layouts[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks']); + } + } - $this->value = $layouts; - } + $this->value = $layouts; + } - public function attrsForm(array $input = []) - { - $settings = $this->settings(); + public function attrsForm(array $input = []) + { + $settings = $this->settings(); - return new Form([ - 'fields' => $settings ? $settings->fields() : [], - 'model' => $this->model, - 'strict' => true, - 'values' => $input, - ]); - } + return new Form([ + 'fields' => $settings ? $settings->fields() : [], + 'model' => $this->model, + 'strict' => true, + 'values' => $input, + ]); + } - public function layouts(): ?array - { - return $this->layouts; - } + public function layouts(): ?array + { + return $this->layouts; + } - public function props(): array - { - $settings = $this->settings(); + public function props(): array + { + $settings = $this->settings(); - return array_merge(parent::props(), [ - 'settings' => $settings !== null ? $settings->toArray() : null, - 'layouts' => $this->layouts() - ]); - } + return array_merge(parent::props(), [ + 'settings' => $settings !== null ? $settings->toArray() : null, + 'layouts' => $this->layouts() + ]); + } - public function routes(): array - { - $field = $this; - $routes = parent::routes(); - $routes[] = [ - 'pattern' => 'layout', - 'method' => 'POST', - 'action' => function () use ($field) { - $defaults = $field->attrsForm([])->data(true); - $attrs = $field->attrsForm($defaults)->values(); - $columns = get('columns') ?? ['1/1']; + public function routes(): array + { + $field = $this; + $routes = parent::routes(); + $routes[] = [ + 'pattern' => 'layout', + 'method' => 'POST', + 'action' => function () use ($field) { + $request = App::instance()->request(); - return Layout::factory([ - 'attrs' => $attrs, - 'columns' => array_map(fn ($width) => [ - 'blocks' => [], - 'id' => uuid(), - 'width' => $width, - ], $columns) - ])->toArray(); - }, - ]; + $defaults = $field->attrsForm([])->data(true); + $attrs = $field->attrsForm($defaults)->values(); + $columns = $request->get('columns') ?? ['1/1']; - $routes[] = [ - 'pattern' => 'fields/(:any)/(:all?)', - 'method' => 'ALL', - 'action' => function (string $fieldName, string $path = null) use ($field) { - $form = $field->attrsForm(); - $field = $form->field($fieldName); + return Layout::factory([ + 'attrs' => $attrs, + 'columns' => array_map(fn ($width) => [ + 'blocks' => [], + 'id' => Str::uuid(), + 'width' => $width, + ], $columns) + ])->toArray(); + }, + ]; - $fieldApi = $this->clone([ - 'routes' => $field->api(), - 'data' => array_merge($this->data(), ['field' => $field]) - ]); + $routes[] = [ + 'pattern' => 'fields/(:any)/(:all?)', + 'method' => 'ALL', + 'action' => function (string $fieldName, string $path = null) use ($field) { + $form = $field->attrsForm(); + $field = $form->field($fieldName); - return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); - } - ]; + $fieldApi = $this->clone([ + 'routes' => $field->api(), + 'data' => array_merge($this->data(), ['field' => $field]) + ]); - return $routes; - } + return $fieldApi->call($path, $this->requestMethod(), $this->requestData()); + } + ]; - protected function setLayouts(array $layouts = []) - { - $this->layouts = array_map( - fn ($layout) => Str::split($layout), - $layouts - ); - } + return $routes; + } - protected function setSettings($settings = null) - { - if (empty($settings) === true) { - $this->settings = null; - return; - } + protected function setLayouts(array $layouts = []) + { + $this->layouts = array_map( + fn ($layout) => Str::split($layout), + $layouts + ); + } - $settings = Blueprint::extend($settings); + protected function setSettings($settings = null) + { + if (empty($settings) === true) { + $this->settings = null; + return; + } - $settings['icon'] = 'dashboard'; - $settings['type'] = 'layout'; - $settings['parent'] = $this->model(); + $settings = Blueprint::extend($settings); - $this->settings = Fieldset::factory($settings); - } + $settings['icon'] = 'dashboard'; + $settings['type'] = 'layout'; + $settings['parent'] = $this->model(); - public function settings() - { - return $this->settings; - } + $this->settings = Fieldset::factory($settings); + } - public function store($value) - { - $value = Layouts::factory($value, ['parent' => $this->model])->toArray(); + public function settings() + { + return $this->settings; + } - // returns empty string to avoid storing empty array as string `[]` - // and to consistency work with `$field->isEmpty()` - if (empty($value) === true) { - return ''; - } + public function store($value) + { + $value = Layouts::factory($value, ['parent' => $this->model])->toArray(); - foreach ($value as $layoutIndex => $layout) { - if ($this->settings !== null) { - $value[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->content(); - } + // returns empty string to avoid storing empty array as string `[]` + // and to consistency work with `$field->isEmpty()` + if (empty($value) === true) { + return ''; + } - foreach ($layout['columns'] as $columnIndex => $column) { - $value[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks'] ?? [], 'content'); - } - } + foreach ($value as $layoutIndex => $layout) { + if ($this->settings !== null) { + $value[$layoutIndex]['attrs'] = $this->attrsForm($layout['attrs'])->content(); + } - return $this->valueToJson($value, $this->pretty()); - } + foreach ($layout['columns'] as $columnIndex => $column) { + $value[$layoutIndex]['columns'][$columnIndex]['blocks'] = $this->blocksToValues($column['blocks'] ?? [], 'content'); + } + } - public function validations(): array - { - return [ - 'layout' => function ($value) { - $fields = []; - $layoutIndex = 0; + return $this->valueToJson($value, $this->pretty()); + } - foreach ($value as $layout) { - $layoutIndex++; + public function validations(): array + { + return [ + 'layout' => function ($value) { + $fields = []; + $layoutIndex = 0; - // validate settings form - foreach ($this->attrsForm($layout['attrs'] ?? [])->fields() as $field) { - $errors = $field->errors(); + foreach ($value as $layout) { + $layoutIndex++; - if (empty($errors) === false) { - throw new InvalidArgumentException([ - 'key' => 'layout.validation.settings', - 'data' => [ - 'index' => $layoutIndex - ] - ]); - } - } + // validate settings form + foreach ($this->attrsForm($layout['attrs'] ?? [])->fields() as $field) { + $errors = $field->errors(); - // validate blocks in the layout - $blockIndex = 0; + if (empty($errors) === false) { + throw new InvalidArgumentException([ + 'key' => 'layout.validation.settings', + 'data' => [ + 'index' => $layoutIndex + ] + ]); + } + } - foreach ($layout['columns'] ?? [] as $column) { - foreach ($column['blocks'] ?? [] as $block) { - $blockIndex++; - $blockType = $block['type']; + // validate blocks in the layout + $blockIndex = 0; - try { - $blockFields = $fields[$blockType] ?? $this->fields($blockType) ?? []; - } catch (Throwable $e) { - // skip invalid blocks - continue; - } + foreach ($layout['columns'] ?? [] as $column) { + foreach ($column['blocks'] ?? [] as $block) { + $blockIndex++; + $blockType = $block['type']; - // store the fields for the next round - $fields[$blockType] = $blockFields; + try { + $blockFields = $fields[$blockType] ?? $this->fields($blockType) ?? []; + } catch (Throwable $e) { + // skip invalid blocks + continue; + } - // overwrite the content with the serialized form - foreach ($this->form($blockFields, $block['content'])->fields() as $field) { - $errors = $field->errors(); + // store the fields for the next round + $fields[$blockType] = $blockFields; - // rough first validation - if (empty($errors) === false) { - throw new InvalidArgumentException([ - 'key' => 'layout.validation.block', - 'data' => [ - 'blockIndex' => $blockIndex, - 'layoutIndex' => $layoutIndex - ] - ]); - } - } - } - } - } + // overwrite the content with the serialized form + foreach ($this->form($blockFields, $block['content'])->fields() as $field) { + $errors = $field->errors(); - return true; - } - ]; - } + // rough first validation + if (empty($errors) === false) { + throw new InvalidArgumentException([ + 'key' => 'layout.validation.block', + 'data' => [ + 'blockIndex' => $blockIndex, + 'layoutIndex' => $layoutIndex + ] + ]); + } + } + } + } + } + + return true; + } + ]; + } } diff --git a/kirby/src/Form/FieldClass.php b/kirby/src/Form/FieldClass.php index 84fc63c..ed3804b 100644 --- a/kirby/src/Form/FieldClass.php +++ b/kirby/src/Form/FieldClass.php @@ -3,6 +3,7 @@ namespace Kirby\Form; use Exception; +use Kirby\Cms\App; use Kirby\Cms\HasSiblings; use Kirby\Cms\ModelWithContent; use Kirby\Data\Data; @@ -23,853 +24,853 @@ use Throwable; */ abstract class FieldClass { - use HasSiblings; - - /** - * @var string|null - */ - protected $after; - - /** - * @var bool - */ - protected $autofocus; - - /** - * @var string|null - */ - protected $before; - - /** - * @var mixed - */ - protected $default; - - /** - * @var bool - */ - protected $disabled; - - /** - * @var string|null - */ - protected $help; - - /** - * @var string|null - */ - protected $icon; - - /** - * @var string|null - */ - protected $label; - - /** - * @var \Kirby\Cms\ModelWithContent - */ - protected $model; - - /** - * @var string - */ - protected $name; - - /** - * @var array - */ - protected $params; - - /** - * @var string|null - */ - protected $placeholder; - - /** - * @var bool - */ - protected $required; - - /** - * @var \Kirby\Form\Fields - */ - protected $siblings; - - /** - * @var bool - */ - protected $translate; - - /** - * @var mixed - */ - protected $value; - - /** - * @var array|null - */ - protected $when; - - /** - * @var string|null - */ - protected $width; - - /** - * @param string $param - * @param array $args - * @return mixed - */ - public function __call(string $param, array $args) - { - if (isset($this->$param) === true) { - return $this->$param; - } - - return $this->params[$param] ?? null; - } - - /** - * @param array $params - */ - public function __construct(array $params = []) - { - $this->params = $params; - - $this->setAfter($params['after'] ?? null); - $this->setAutofocus($params['autofocus'] ?? false); - $this->setBefore($params['before'] ?? null); - $this->setDefault($params['default'] ?? null); - $this->setDisabled($params['disabled'] ?? false); - $this->setHelp($params['help'] ?? null); - $this->setIcon($params['icon'] ?? null); - $this->setLabel($params['label'] ?? null); - $this->setModel($params['model'] ?? site()); - $this->setName($params['name'] ?? null); - $this->setPlaceholder($params['placeholder'] ?? null); - $this->setRequired($params['required'] ?? false); - $this->setSiblings($params['siblings'] ?? null); - $this->setTranslate($params['translate'] ?? true); - $this->setWhen($params['when'] ?? null); - $this->setWidth($params['width'] ?? null); - - if (array_key_exists('value', $params) === true) { - $this->fill($params['value']); - } - } - - /** - * @return string|null - */ - public function after(): ?string - { - return $this->stringTemplate($this->after); - } - - /** - * @return array - */ - public function api(): array - { - return $this->routes(); - } - - /** - * @return bool - */ - public function autofocus(): bool - { - return $this->autofocus; - } - - /** - * @return string|null - */ - public function before(): ?string - { - return $this->stringTemplate($this->before); - } - - /** - * @deprecated 3.5.0 - * @todo remove when the general field class setup has been refactored - * - * Returns the field data - * in a format to be stored - * in Kirby's content fields - * - * @param bool $default - * @return mixed - */ - public function data(bool $default = false) - { - return $this->store($this->value($default)); - } - - /** - * Returns the default value for the field, - * which will be used when a page/file/user is created - * - * @return mixed - */ - public function default() - { - if (is_string($this->default) === false) { - return $this->default; - } - - return $this->stringTemplate($this->default); - } - - /** - * If `true`, the field is no longer editable and will not be saved - * - * @return bool - */ - public function disabled(): bool - { - return $this->disabled; - } - - /** - * Runs all validations and returns an array of - * error messages - * - * @return array - */ - public function errors(): array - { - return $this->validate(); - } - - /** - * Setter for the value - * - * @param mixed $value - * @return void - */ - public function fill($value = null) - { - $this->value = $value; - } - - /** - * Optional help text below the field - * - * @return string|null - */ - public function help(): ?string - { - if (empty($this->help) === false) { - $help = $this->stringTemplate($this->help); - $help = $this->kirby()->kirbytext($help); - return $help; - } - - return null; - } - - /** - * @param string|array|null $param - * @return string|null - */ - protected function i18n($param = null): ?string - { - return empty($param) === false ? I18n::translate($param, $param) : null; - } - - /** - * Optional icon that will be shown at the end of the field - * - * @return string|null - */ - public function icon(): ?string - { - return $this->icon; - } - - /** - * @return string - */ - public function id(): string - { - return $this->name(); - } - - /** - * @return bool - */ - public function isDisabled(): bool - { - return $this->disabled; - } - - /** - * @return bool - */ - public function isEmpty(): bool - { - return $this->isEmptyValue($this->value()); - } - - /** - * @param mixed $value - * @return bool - */ - public function isEmptyValue($value = null): bool - { - return in_array($value, [null, '', []], true); - } - - /** - * Checks if the field is invalid - * - * @return bool - */ - public function isInvalid(): bool - { - return $this->isValid() === false; - } - - /** - * @return bool - */ - public function isRequired(): bool - { - return $this->required; - } - - /** - * @return bool - */ - public function isSaveable(): bool - { - return true; - } - - /** - * Checks if the field is valid - * - * @return bool - */ - public function isValid(): bool - { - return empty($this->errors()) === true; - } - - /** - * Returns the Kirby instance - * - * @return \Kirby\Cms\App - */ - public function kirby() - { - return $this->model->kirby(); - } - - /** - * The field label can be set as string or associative array with translations - * - * @return string - */ - public function label(): string - { - return $this->stringTemplate($this->label ?? Str::ucfirst($this->name())); - } - - /** - * Returns the parent model - * - * @return mixed - */ - public function model() - { - return $this->model; - } - - /** - * Returns the field name - * - * @return string - */ - public function name(): string - { - return $this->name ?? $this->type(); - } - - /** - * Checks if the field needs a value before being saved; - * this is the case if all of the following requirements are met: - * - The field is saveable - * - The field is required - * - The field is currently empty - * - The field is not currently inactive because of a `when` rule - * - * @return bool - */ - protected function needsValue(): bool - { - // check simple conditions first - if ( - $this->isSaveable() === false || - $this->isRequired() === false || - $this->isEmpty() === false - ) { - return false; - } - - // check the data of the relevant fields if there is a `when` option - if (empty($this->when) === false && is_array($this->when) === true) { - $formFields = $this->siblings(); - - if ($formFields !== null) { - foreach ($this->when as $field => $value) { - $field = $formFields->get($field); - $inputValue = $field !== null ? $field->value() : ''; - - // if the input data doesn't match the requested `when` value, - // that means that this field is not required and can be saved - // (*all* `when` conditions must be met for this field to be required) - if ($inputValue !== $value) { - return false; - } - } - } - } - - // either there was no `when` condition or all conditions matched - return true; - } - - /** - * Returns all original params for the field - * - * @return array - */ - public function params(): array - { - return $this->params; - } - - /** - * Optional placeholder value that will be shown when the field is empty - * - * @return string|null - */ - public function placeholder(): ?string - { - return $this->stringTemplate($this->placeholder); - } - - /** - * Define the props that will be sent to - * the Vue component - * - * @return array - */ - public function props(): array - { - return [ - 'after' => $this->after(), - 'autofocus' => $this->autofocus(), - 'before' => $this->before(), - 'default' => $this->default(), - 'disabled' => $this->isDisabled(), - 'help' => $this->help(), - 'icon' => $this->icon(), - 'label' => $this->label(), - 'name' => $this->name(), - 'placeholder' => $this->placeholder(), - 'required' => $this->isRequired(), - 'saveable' => $this->isSaveable(), - 'translate' => $this->translate(), - 'type' => $this->type(), - 'when' => $this->when(), - 'width' => $this->width(), - ]; - } - - /** - * If `true`, the field has to be filled in correctly to be saved. - * - * @return bool - */ - public function required(): bool - { - return $this->required; - } - - /** - * Routes for the field API - * - * @return array - */ - public function routes(): array - { - return []; - } - - /** - * @deprecated 3.5.0 - * @todo remove when the general field class setup has been refactored - * @return bool - */ - public function save() - { - return $this->isSaveable(); - } - - /** - * @param array|string|null $after - * @return void - */ - protected function setAfter($after = null) - { - $this->after = $this->i18n($after); - } - - /** - * @param bool $autofocus - * @return void - */ - protected function setAutofocus(bool $autofocus = false) - { - $this->autofocus = $autofocus; - } - - /** - * @param array|string|null $before - * @return void - */ - protected function setBefore($before = null) - { - $this->before = $this->i18n($before); - } - - /** - * @param mixed $default - * @return void - */ - protected function setDefault($default = null) - { - $this->default = $default; - } - - /** - * @param bool $disabled - * @return void - */ - protected function setDisabled(bool $disabled = false) - { - $this->disabled = $disabled; - } - - /** - * @param array|string|null $help - * @return void - */ - protected function setHelp($help = null) - { - $this->help = $this->i18n($help); - } - - /** - * @param string|null $icon - * @return void - */ - protected function setIcon(?string $icon = null) - { - $this->icon = $icon; - } - - /** - * @param array|string|null $label - * @return void - */ - protected function setLabel($label = null) - { - $this->label = $this->i18n($label); - } - - /** - * @param \Kirby\Cms\ModelWithContent $model - * @return void - */ - protected function setModel(ModelWithContent $model) - { - $this->model = $model; - } - - /** - * @param string|null $name - * @return void - */ - protected function setName(string $name = null) - { - $this->name = $name; - } - - /** - * @param array|string|null $placeholder - * @return void - */ - protected function setPlaceholder($placeholder = null) - { - $this->placeholder = $this->i18n($placeholder); - } - - /** - * @param bool $required - * @return void - */ - protected function setRequired(bool $required = false) - { - $this->required = $required; - } - - /** - * @param \Kirby\Form\Fields|null $siblings - * @return void - */ - protected function setSiblings(?Fields $siblings = null) - { - $this->siblings = $siblings ?? new Fields([$this]); - } - - /** - * @param bool $translate - * @return void - */ - protected function setTranslate(bool $translate = true) - { - $this->translate = $translate; - } - - /** - * Setter for the when condition - * - * @param mixed $when - * @return void - */ - protected function setWhen($when = null) - { - $this->when = $when; - } - - /** - * Setter for the field width - * - * @param string|null $width - * @return void - */ - protected function setWidth(string $width = null) - { - $this->width = $width; - } - - /** - * Returns all sibling fields - * - * @return \Kirby\Form\Fields - */ - protected function siblingsCollection() - { - return $this->siblings; - } - - /** - * Parses a string template in the given value - * - * @param string|null $string - * @return string|null - */ - protected function stringTemplate(?string $string = null): ?string - { - if ($string !== null) { - return $this->model->toString($string); - } - - return null; - } - - /** - * Converts the given value to a value - * that can be stored in the text file - * - * @param mixed $value - * @return mixed - */ - public function store($value) - { - return $value; - } - - /** - * Should the field be translatable? - * - * @return bool - */ - public function translate(): bool - { - return $this->translate; - } - - /** - * Converts the field to a plain array - * - * @return array - */ - public function toArray(): array - { - $props = $this->props(); - $props['signature'] = md5(json_encode($props)); - - ksort($props); - - return array_filter($props, fn ($item) => $item !== null); - } - - /** - * Returns the field type - * - * @return string - */ - public function type(): string - { - return lcfirst(basename(str_replace(['\\', 'Field'], ['/', ''], static::class))); - } - - /** - * Runs the validations defined for the field - * - * @return array - */ - protected function validate(): array - { - $validations = $this->validations(); - $value = $this->value(); - $errors = []; - - // validate required values - if ($this->needsValue() === true) { - $errors['required'] = I18n::translate('error.validation.required'); - } - - foreach ($validations as $key => $validation) { - if (is_int($key) === true) { - // predefined validation - try { - Validations::$validation($this, $value); - } catch (Exception $e) { - $errors[$validation] = $e->getMessage(); - } - continue; - } - - if (is_a($validation, 'Closure') === true) { - try { - $validation->call($this, $value); - } catch (Exception $e) { - $errors[$key] = $e->getMessage(); - } - } - } - - return $errors; - } - - /** - * Defines all validation rules - * - * @return array - */ - protected function validations(): array - { - return []; - } - - /** - * Returns the value of the field if saveable - * otherwise it returns null - * - * @return mixed - */ - public function value(bool $default = false) - { - if ($this->isSaveable() === false) { - return null; - } - - if ($default === true && $this->isEmpty() === true) { - return $this->default(); - } - - return $this->value; - } - - /** - * @param mixed $value - * @return array - */ - protected function valueFromJson($value): array - { - try { - return Data::decode($value, 'json'); - } catch (Throwable $e) { - return []; - } - } - - /** - * @param mixed $value - * @return array - */ - protected function valueFromYaml($value): array - { - return Data::decode($value, 'yaml'); - } - - /** - * @param array|null $value - * @param bool $pretty - * @return string - */ - protected function valueToJson(array $value = null, bool $pretty = false): string - { - if ($pretty === true) { - return json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - } - - return json_encode($value); - } - - /** - * @param array|null $value - * @return string - */ - protected function valueToYaml(array $value = null): string - { - return Data::encode($value, 'yaml'); - } - - /** - * Conditions when the field will be shown - * - * @return array|null - */ - public function when(): ?array - { - return $this->when; - } - - /** - * Returns the width of the field in - * the Panel grid - * - * @return string - */ - public function width(): string - { - return $this->width ?? '1/1'; - } + use HasSiblings; + + /** + * @var string|null + */ + protected $after; + + /** + * @var bool + */ + protected $autofocus; + + /** + * @var string|null + */ + protected $before; + + /** + * @var mixed + */ + protected $default; + + /** + * @var bool + */ + protected $disabled; + + /** + * @var string|null + */ + protected $help; + + /** + * @var string|null + */ + protected $icon; + + /** + * @var string|null + */ + protected $label; + + /** + * @var \Kirby\Cms\ModelWithContent + */ + protected $model; + + /** + * @var string + */ + protected $name; + + /** + * @var array + */ + protected $params; + + /** + * @var string|null + */ + protected $placeholder; + + /** + * @var bool + */ + protected $required; + + /** + * @var \Kirby\Form\Fields + */ + protected $siblings; + + /** + * @var bool + */ + protected $translate; + + /** + * @var mixed + */ + protected $value; + + /** + * @var array|null + */ + protected $when; + + /** + * @var string|null + */ + protected $width; + + /** + * @param string $param + * @param array $args + * @return mixed + */ + public function __call(string $param, array $args) + { + if (isset($this->$param) === true) { + return $this->$param; + } + + return $this->params[$param] ?? null; + } + + /** + * @param array $params + */ + public function __construct(array $params = []) + { + $this->params = $params; + + $this->setAfter($params['after'] ?? null); + $this->setAutofocus($params['autofocus'] ?? false); + $this->setBefore($params['before'] ?? null); + $this->setDefault($params['default'] ?? null); + $this->setDisabled($params['disabled'] ?? false); + $this->setHelp($params['help'] ?? null); + $this->setIcon($params['icon'] ?? null); + $this->setLabel($params['label'] ?? null); + $this->setModel($params['model'] ?? App::instance()->site()); + $this->setName($params['name'] ?? null); + $this->setPlaceholder($params['placeholder'] ?? null); + $this->setRequired($params['required'] ?? false); + $this->setSiblings($params['siblings'] ?? null); + $this->setTranslate($params['translate'] ?? true); + $this->setWhen($params['when'] ?? null); + $this->setWidth($params['width'] ?? null); + + if (array_key_exists('value', $params) === true) { + $this->fill($params['value']); + } + } + + /** + * @return string|null + */ + public function after(): ?string + { + return $this->stringTemplate($this->after); + } + + /** + * @return array + */ + public function api(): array + { + return $this->routes(); + } + + /** + * @return bool + */ + public function autofocus(): bool + { + return $this->autofocus; + } + + /** + * @return string|null + */ + public function before(): ?string + { + return $this->stringTemplate($this->before); + } + + /** + * @deprecated 3.5.0 + * @todo remove when the general field class setup has been refactored + * + * Returns the field data + * in a format to be stored + * in Kirby's content fields + * + * @param bool $default + * @return mixed + */ + public function data(bool $default = false) + { + return $this->store($this->value($default)); + } + + /** + * Returns the default value for the field, + * which will be used when a page/file/user is created + * + * @return mixed + */ + public function default() + { + if (is_string($this->default) === false) { + return $this->default; + } + + return $this->stringTemplate($this->default); + } + + /** + * If `true`, the field is no longer editable and will not be saved + * + * @return bool + */ + public function disabled(): bool + { + return $this->disabled; + } + + /** + * Runs all validations and returns an array of + * error messages + * + * @return array + */ + public function errors(): array + { + return $this->validate(); + } + + /** + * Setter for the value + * + * @param mixed $value + * @return void + */ + public function fill($value = null) + { + $this->value = $value; + } + + /** + * Optional help text below the field + * + * @return string|null + */ + public function help(): ?string + { + if (empty($this->help) === false) { + $help = $this->stringTemplate($this->help); + $help = $this->kirby()->kirbytext($help); + return $help; + } + + return null; + } + + /** + * @param string|array|null $param + * @return string|null + */ + protected function i18n($param = null): ?string + { + return empty($param) === false ? I18n::translate($param, $param) : null; + } + + /** + * Optional icon that will be shown at the end of the field + * + * @return string|null + */ + public function icon(): ?string + { + return $this->icon; + } + + /** + * @return string + */ + public function id(): string + { + return $this->name(); + } + + /** + * @return bool + */ + public function isDisabled(): bool + { + return $this->disabled; + } + + /** + * @return bool + */ + public function isEmpty(): bool + { + return $this->isEmptyValue($this->value()); + } + + /** + * @param mixed $value + * @return bool + */ + public function isEmptyValue($value = null): bool + { + return in_array($value, [null, '', []], true); + } + + /** + * Checks if the field is invalid + * + * @return bool + */ + public function isInvalid(): bool + { + return $this->isValid() === false; + } + + /** + * @return bool + */ + public function isRequired(): bool + { + return $this->required; + } + + /** + * @return bool + */ + public function isSaveable(): bool + { + return true; + } + + /** + * Checks if the field is valid + * + * @return bool + */ + public function isValid(): bool + { + return empty($this->errors()) === true; + } + + /** + * Returns the Kirby instance + * + * @return \Kirby\Cms\App + */ + public function kirby() + { + return $this->model->kirby(); + } + + /** + * The field label can be set as string or associative array with translations + * + * @return string + */ + public function label(): string + { + return $this->stringTemplate($this->label ?? Str::ucfirst($this->name())); + } + + /** + * Returns the parent model + * + * @return mixed + */ + public function model() + { + return $this->model; + } + + /** + * Returns the field name + * + * @return string + */ + public function name(): string + { + return $this->name ?? $this->type(); + } + + /** + * Checks if the field needs a value before being saved; + * this is the case if all of the following requirements are met: + * - The field is saveable + * - The field is required + * - The field is currently empty + * - The field is not currently inactive because of a `when` rule + * + * @return bool + */ + protected function needsValue(): bool + { + // check simple conditions first + if ( + $this->isSaveable() === false || + $this->isRequired() === false || + $this->isEmpty() === false + ) { + return false; + } + + // check the data of the relevant fields if there is a `when` option + if (empty($this->when) === false && is_array($this->when) === true) { + $formFields = $this->siblings(); + + if ($formFields !== null) { + foreach ($this->when as $field => $value) { + $field = $formFields->get($field); + $inputValue = $field !== null ? $field->value() : ''; + + // if the input data doesn't match the requested `when` value, + // that means that this field is not required and can be saved + // (*all* `when` conditions must be met for this field to be required) + if ($inputValue !== $value) { + return false; + } + } + } + } + + // either there was no `when` condition or all conditions matched + return true; + } + + /** + * Returns all original params for the field + * + * @return array + */ + public function params(): array + { + return $this->params; + } + + /** + * Optional placeholder value that will be shown when the field is empty + * + * @return string|null + */ + public function placeholder(): ?string + { + return $this->stringTemplate($this->placeholder); + } + + /** + * Define the props that will be sent to + * the Vue component + * + * @return array + */ + public function props(): array + { + return [ + 'after' => $this->after(), + 'autofocus' => $this->autofocus(), + 'before' => $this->before(), + 'default' => $this->default(), + 'disabled' => $this->isDisabled(), + 'help' => $this->help(), + 'icon' => $this->icon(), + 'label' => $this->label(), + 'name' => $this->name(), + 'placeholder' => $this->placeholder(), + 'required' => $this->isRequired(), + 'saveable' => $this->isSaveable(), + 'translate' => $this->translate(), + 'type' => $this->type(), + 'when' => $this->when(), + 'width' => $this->width(), + ]; + } + + /** + * If `true`, the field has to be filled in correctly to be saved. + * + * @return bool + */ + public function required(): bool + { + return $this->required; + } + + /** + * Routes for the field API + * + * @return array + */ + public function routes(): array + { + return []; + } + + /** + * @deprecated 3.5.0 + * @todo remove when the general field class setup has been refactored + * @return bool + */ + public function save() + { + return $this->isSaveable(); + } + + /** + * @param array|string|null $after + * @return void + */ + protected function setAfter($after = null) + { + $this->after = $this->i18n($after); + } + + /** + * @param bool $autofocus + * @return void + */ + protected function setAutofocus(bool $autofocus = false) + { + $this->autofocus = $autofocus; + } + + /** + * @param array|string|null $before + * @return void + */ + protected function setBefore($before = null) + { + $this->before = $this->i18n($before); + } + + /** + * @param mixed $default + * @return void + */ + protected function setDefault($default = null) + { + $this->default = $default; + } + + /** + * @param bool $disabled + * @return void + */ + protected function setDisabled(bool $disabled = false) + { + $this->disabled = $disabled; + } + + /** + * @param array|string|null $help + * @return void + */ + protected function setHelp($help = null) + { + $this->help = $this->i18n($help); + } + + /** + * @param string|null $icon + * @return void + */ + protected function setIcon(?string $icon = null) + { + $this->icon = $icon; + } + + /** + * @param array|string|null $label + * @return void + */ + protected function setLabel($label = null) + { + $this->label = $this->i18n($label); + } + + /** + * @param \Kirby\Cms\ModelWithContent $model + * @return void + */ + protected function setModel(ModelWithContent $model) + { + $this->model = $model; + } + + /** + * @param string|null $name + * @return void + */ + protected function setName(string $name = null) + { + $this->name = $name; + } + + /** + * @param array|string|null $placeholder + * @return void + */ + protected function setPlaceholder($placeholder = null) + { + $this->placeholder = $this->i18n($placeholder); + } + + /** + * @param bool $required + * @return void + */ + protected function setRequired(bool $required = false) + { + $this->required = $required; + } + + /** + * @param \Kirby\Form\Fields|null $siblings + * @return void + */ + protected function setSiblings(?Fields $siblings = null) + { + $this->siblings = $siblings ?? new Fields([$this]); + } + + /** + * @param bool $translate + * @return void + */ + protected function setTranslate(bool $translate = true) + { + $this->translate = $translate; + } + + /** + * Setter for the when condition + * + * @param mixed $when + * @return void + */ + protected function setWhen($when = null) + { + $this->when = $when; + } + + /** + * Setter for the field width + * + * @param string|null $width + * @return void + */ + protected function setWidth(string $width = null) + { + $this->width = $width; + } + + /** + * Returns all sibling fields + * + * @return \Kirby\Form\Fields + */ + protected function siblingsCollection() + { + return $this->siblings; + } + + /** + * Parses a string template in the given value + * + * @param string|null $string + * @return string|null + */ + protected function stringTemplate(?string $string = null): ?string + { + if ($string !== null) { + return $this->model->toString($string); + } + + return null; + } + + /** + * Converts the given value to a value + * that can be stored in the text file + * + * @param mixed $value + * @return mixed + */ + public function store($value) + { + return $value; + } + + /** + * Should the field be translatable? + * + * @return bool + */ + public function translate(): bool + { + return $this->translate; + } + + /** + * Converts the field to a plain array + * + * @return array + */ + public function toArray(): array + { + $props = $this->props(); + $props['signature'] = md5(json_encode($props)); + + ksort($props); + + return array_filter($props, fn ($item) => $item !== null); + } + + /** + * Returns the field type + * + * @return string + */ + public function type(): string + { + return lcfirst(basename(str_replace(['\\', 'Field'], ['/', ''], static::class))); + } + + /** + * Runs the validations defined for the field + * + * @return array + */ + protected function validate(): array + { + $validations = $this->validations(); + $value = $this->value(); + $errors = []; + + // validate required values + if ($this->needsValue() === true) { + $errors['required'] = I18n::translate('error.validation.required'); + } + + foreach ($validations as $key => $validation) { + if (is_int($key) === true) { + // predefined validation + try { + Validations::$validation($this, $value); + } catch (Exception $e) { + $errors[$validation] = $e->getMessage(); + } + continue; + } + + if (is_a($validation, 'Closure') === true) { + try { + $validation->call($this, $value); + } catch (Exception $e) { + $errors[$key] = $e->getMessage(); + } + } + } + + return $errors; + } + + /** + * Defines all validation rules + * + * @return array + */ + protected function validations(): array + { + return []; + } + + /** + * Returns the value of the field if saveable + * otherwise it returns null + * + * @return mixed + */ + public function value(bool $default = false) + { + if ($this->isSaveable() === false) { + return null; + } + + if ($default === true && $this->isEmpty() === true) { + return $this->default(); + } + + return $this->value; + } + + /** + * @param mixed $value + * @return array + */ + protected function valueFromJson($value): array + { + try { + return Data::decode($value, 'json'); + } catch (Throwable $e) { + return []; + } + } + + /** + * @param mixed $value + * @return array + */ + protected function valueFromYaml($value): array + { + return Data::decode($value, 'yaml'); + } + + /** + * @param array|null $value + * @param bool $pretty + * @return string + */ + protected function valueToJson(array $value = null, bool $pretty = false): string + { + if ($pretty === true) { + return json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + } + + return json_encode($value); + } + + /** + * @param array|null $value + * @return string + */ + protected function valueToYaml(array $value = null): string + { + return Data::encode($value, 'yaml'); + } + + /** + * Conditions when the field will be shown + * + * @return array|null + */ + public function when(): ?array + { + return $this->when; + } + + /** + * Returns the width of the field in + * the Panel grid + * + * @return string + */ + public function width(): string + { + return $this->width ?? '1/1'; + } } diff --git a/kirby/src/Form/Fields.php b/kirby/src/Form/Fields.php index b6b3e36..9abd199 100644 --- a/kirby/src/Form/Fields.php +++ b/kirby/src/Form/Fields.php @@ -16,42 +16,42 @@ use Kirby\Toolkit\Collection; */ class Fields extends Collection { - /** - * Internal setter for each object in the Collection. - * This takes care of validation and of setting - * the collection prop on each object correctly. - * - * @param string $name - * @param object|array $field - * @return $this - */ - public function __set(string $name, $field) - { - if (is_array($field) === true) { - // use the array key as name if the name is not set - $field['name'] ??= $name; - $field = Field::factory($field['type'], $field, $this); - } + /** + * Internal setter for each object in the Collection. + * This takes care of validation and of setting + * the collection prop on each object correctly. + * + * @param string $name + * @param object|array $field + * @return void + */ + public function __set(string $name, $field): void + { + if (is_array($field) === true) { + // use the array key as name if the name is not set + $field['name'] ??= $name; + $field = Field::factory($field['type'], $field, $this); + } - return parent::__set($field->name(), $field); - } + parent::__set($field->name(), $field); + } - /** - * Converts the fields collection to an - * array and also does that for every - * included field. - * - * @param \Closure|null $map - * @return array - */ - public function toArray(Closure $map = null): array - { - $array = []; + /** + * Converts the fields collection to an + * array and also does that for every + * included field. + * + * @param \Closure|null $map + * @return array + */ + public function toArray(Closure $map = null): array + { + $array = []; - foreach ($this as $field) { - $array[$field->name()] = $field->toArray(); - } + foreach ($this as $field) { + $array[$field->name()] = $field->toArray(); + } - return $array; - } + return $array; + } } diff --git a/kirby/src/Form/Form.php b/kirby/src/Form/Form.php index e77006b..5935ced 100644 --- a/kirby/src/Form/Form.php +++ b/kirby/src/Form/Form.php @@ -23,373 +23,371 @@ use Throwable; */ class Form { - /** - * An array of all found errors - * - * @var array|null - */ - protected $errors; + /** + * An array of all found errors + * + * @var array|null + */ + protected $errors; - /** - * Fields in the form - * - * @var \Kirby\Form\Fields|null - */ - protected $fields; + /** + * Fields in the form + * + * @var \Kirby\Form\Fields|null + */ + protected $fields; - /** - * All values of form - * - * @var array - */ - protected $values = []; + /** + * All values of form + * + * @var array + */ + protected $values = []; - /** - * Form constructor - * - * @param array $props - */ - public function __construct(array $props) - { - $fields = $props['fields'] ?? []; - $values = $props['values'] ?? []; - $input = $props['input'] ?? []; - $strict = $props['strict'] ?? false; - $inject = $props; + /** + * Form constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $fields = $props['fields'] ?? []; + $values = $props['values'] ?? []; + $input = $props['input'] ?? []; + $strict = $props['strict'] ?? false; + $inject = $props; - // prepare field properties for multilang setups - $fields = static::prepareFieldsForLanguage( - $fields, - $props['language'] ?? null - ); + // prepare field properties for multilang setups + $fields = static::prepareFieldsForLanguage( + $fields, + $props['language'] ?? null + ); - // lowercase all value names - $values = array_change_key_case($values); - $input = array_change_key_case($input); + // lowercase all value names + $values = array_change_key_case($values); + $input = array_change_key_case($input); - unset($inject['fields'], $inject['values'], $inject['input']); + unset($inject['fields'], $inject['values'], $inject['input']); - $this->fields = new Fields(); - $this->values = []; + $this->fields = new Fields(); + $this->values = []; - foreach ($fields as $name => $props) { + foreach ($fields as $name => $props) { + // inject stuff from the form constructor (model, etc.) + $props = array_merge($inject, $props); - // inject stuff from the form constructor (model, etc.) - $props = array_merge($inject, $props); + // inject the name + $props['name'] = $name = strtolower($name); - // inject the name - $props['name'] = $name = strtolower($name); + // check if the field is disabled + $disabled = $props['disabled'] ?? false; - // check if the field is disabled - $disabled = $props['disabled'] ?? false; + // overwrite the field value if not set + if ($disabled === true) { + $props['value'] = $values[$name] ?? null; + } else { + $props['value'] = $input[$name] ?? $values[$name] ?? null; + } - // overwrite the field value if not set - if ($disabled === true) { - $props['value'] = $values[$name] ?? null; - } else { - $props['value'] = $input[$name] ?? $values[$name] ?? null; - } + try { + $field = Field::factory($props['type'], $props, $this->fields); + } catch (Throwable $e) { + $field = static::exceptionField($e, $props); + } - try { - $field = Field::factory($props['type'], $props, $this->fields); - } catch (Throwable $e) { - $field = static::exceptionField($e, $props); - } + if ($field->save() !== false) { + $this->values[$name] = $field->value(); + } - if ($field->save() !== false) { - $this->values[$name] = $field->value(); - } + $this->fields->append($name, $field); + } - $this->fields->append($name, $field); - } + if ($strict !== true) { + // use all given values, no matter + // if there's a field or not. + $input = array_merge($values, $input); - if ($strict !== true) { + foreach ($input as $key => $value) { + if (isset($this->values[$key]) === false) { + $this->values[$key] = $value; + } + } + } + } - // use all given values, no matter - // if there's a field or not. - $input = array_merge($values, $input); + /** + * Returns the data required to write to the content file + * Doesn't include default and null values + * + * @return array + */ + public function content(): array + { + return $this->data(false, false); + } - foreach ($input as $key => $value) { - if (isset($this->values[$key]) === false) { - $this->values[$key] = $value; - } - } - } - } + /** + * Returns data for all fields in the form + * + * @param false $defaults + * @param bool $includeNulls + * @return array + */ + public function data($defaults = false, bool $includeNulls = true): array + { + $data = $this->values; - /** - * Returns the data required to write to the content file - * Doesn't include default and null values - * - * @return array - */ - public function content(): array - { - return $this->data(false, false); - } + foreach ($this->fields as $field) { + if ($field->save() === false || $field->unset() === true) { + if ($includeNulls === true) { + $data[$field->name()] = null; + } else { + unset($data[$field->name()]); + } + } else { + $data[$field->name()] = $field->data($defaults); + } + } - /** - * Returns data for all fields in the form - * - * @param false $defaults - * @param bool $includeNulls - * @return array - */ - public function data($defaults = false, bool $includeNulls = true): array - { - $data = $this->values; + return $data; + } - foreach ($this->fields as $field) { - if ($field->save() === false || $field->unset() === true) { - if ($includeNulls === true) { - $data[$field->name()] = null; - } else { - unset($data[$field->name()]); - } - } else { - $data[$field->name()] = $field->data($defaults); - } - } + /** + * An array of all found errors + * + * @return array + */ + public function errors(): array + { + if ($this->errors !== null) { + return $this->errors; + } - return $data; - } + $this->errors = []; - /** - * An array of all found errors - * - * @return array - */ - public function errors(): array - { - if ($this->errors !== null) { - return $this->errors; - } + foreach ($this->fields as $field) { + if (empty($field->errors()) === false) { + $this->errors[$field->name()] = [ + 'label' => $field->label(), + 'message' => $field->errors() + ]; + } + } - $this->errors = []; + return $this->errors; + } - foreach ($this->fields as $field) { - if (empty($field->errors()) === false) { - $this->errors[$field->name()] = [ - 'label' => $field->label(), - 'message' => $field->errors() - ]; - } - } + /** + * Shows the error with the field + * + * @param \Throwable $exception + * @param array $props + * @return \Kirby\Form\Field + */ + public static function exceptionField(Throwable $exception, array $props = []) + { + $message = $exception->getMessage(); - return $this->errors; - } + if (App::instance()->option('debug') === true) { + $message .= ' in file: ' . $exception->getFile() . ' line: ' . $exception->getLine(); + } - /** - * Shows the error with the field - * - * @param \Throwable $exception - * @param array $props - * @return \Kirby\Form\Field - */ - public static function exceptionField(Throwable $exception, array $props = []) - { - $message = $exception->getMessage(); + $props = array_merge($props, [ + 'label' => 'Error in "' . $props['name'] . '" field.', + 'theme' => 'negative', + 'text' => strip_tags($message), + ]); - if (App::instance()->option('debug') === true) { - $message .= ' in file: ' . $exception->getFile() . ' line: ' . $exception->getLine(); - } + return Field::factory('info', $props); + } - $props = array_merge($props, [ - 'label' => 'Error in "' . $props['name'] . '" field.', - 'theme' => 'negative', - 'text' => strip_tags($message), - ]); + /** + * Get the field object by name + * and handle nested fields correctly + * + * @param string $name + * @throws \Kirby\Exception\NotFoundException + * @return \Kirby\Form\Field + */ + public function field(string $name) + { + $form = $this; + $fieldNames = Str::split($name, '+'); + $index = 0; + $count = count($fieldNames); + $field = null; - return Field::factory('info', $props); - } + foreach ($fieldNames as $fieldName) { + $index++; - /** - * Get the field object by name - * and handle nested fields correctly - * - * @param string $name - * @throws \Kirby\Exception\NotFoundException - * @return \Kirby\Form\Field - */ - public function field(string $name) - { - $form = $this; - $fieldNames = Str::split($name, '+'); - $index = 0; - $count = count($fieldNames); - $field = null; + if ($field = $form->fields()->get($fieldName)) { + if ($count !== $index) { + $form = $field->form(); + } + } else { + throw new NotFoundException('The field "' . $fieldName . '" could not be found'); + } + } - foreach ($fieldNames as $fieldName) { - $index++; + // it can get this error only if $name is an empty string as $name = '' + if ($field === null) { + throw new NotFoundException('No field could be loaded'); + } - if ($field = $form->fields()->get($fieldName)) { - if ($count !== $index) { - $form = $field->form(); - } - } else { - throw new NotFoundException('The field "' . $fieldName . '" could not be found'); - } - } + return $field; + } - // it can get this error only if $name is an empty string as $name = '' - if ($field === null) { - throw new NotFoundException('No field could be loaded'); - } + /** + * Returns form fields + * + * @return \Kirby\Form\Fields|null + */ + public function fields() + { + return $this->fields; + } - return $field; - } + /** + * @param \Kirby\Cms\Model $model + * @param array $props + * @return static + */ + public static function for(Model $model, array $props = []) + { + // get the original model data + $original = $model->content($props['language'] ?? null)->toArray(); + $values = $props['values'] ?? []; - /** - * Returns form fields - * - * @return \Kirby\Form\Fields|null - */ - public function fields() - { - return $this->fields; - } + // convert closures to values + foreach ($values as $key => $value) { + if (is_a($value, 'Closure') === true) { + $values[$key] = $value($original[$key] ?? null); + } + } - /** - * @param \Kirby\Cms\Model $model - * @param array $props - * @return static - */ - public static function for(Model $model, array $props = []) - { - // get the original model data - $original = $model->content($props['language'] ?? null)->toArray(); - $values = $props['values'] ?? []; + // set a few defaults + $props['values'] = array_merge($original, $values); + $props['fields'] ??= []; + $props['model'] = $model; - // convert closures to values - foreach ($values as $key => $value) { - if (is_a($value, 'Closure') === true) { - $values[$key] = $value($original[$key] ?? null); - } - } + // search for the blueprint + if (method_exists($model, 'blueprint') === true && $blueprint = $model->blueprint()) { + $props['fields'] = $blueprint->fields(); + } - // set a few defaults - $props['values'] = array_merge($original, $values); - $props['fields'] ??= []; - $props['model'] = $model; + $ignoreDisabled = $props['ignoreDisabled'] ?? false; - // search for the blueprint - if (method_exists($model, 'blueprint') === true && $blueprint = $model->blueprint()) { - $props['fields'] = $blueprint->fields(); - } + // REFACTOR: this could be more elegant + if ($ignoreDisabled === true) { + $props['fields'] = array_map(function ($field) { + $field['disabled'] = false; + return $field; + }, $props['fields']); + } - $ignoreDisabled = $props['ignoreDisabled'] ?? false; + return new static($props); + } - // REFACTOR: this could be more elegant - if ($ignoreDisabled === true) { - $props['fields'] = array_map(function ($field) { - $field['disabled'] = false; - return $field; - }, $props['fields']); - } + /** + * Checks if the form is invalid + * + * @return bool + */ + public function isInvalid(): bool + { + return empty($this->errors()) === false; + } - return new static($props); - } + /** + * Checks if the form is valid + * + * @return bool + */ + public function isValid(): bool + { + return empty($this->errors()) === true; + } - /** - * Checks if the form is invalid - * - * @return bool - */ - public function isInvalid(): bool - { - return empty($this->errors()) === false; - } + /** + * Disables fields in secondary languages when + * they are configured to be untranslatable + * + * @param array $fields + * @param string|null $language + * @return array + */ + protected static function prepareFieldsForLanguage(array $fields, ?string $language = null): array + { + $kirby = App::instance(null, true); - /** - * Checks if the form is valid - * - * @return bool - */ - public function isValid(): bool - { - return empty($this->errors()) === true; - } + // only modify the fields if we have a valid Kirby multilang instance + if (!$kirby || $kirby->multilang() === false) { + return $fields; + } - /** - * Disables fields in secondary languages when - * they are configured to be untranslatable - * - * @param array $fields - * @param string|null $language - * @return array - */ - protected static function prepareFieldsForLanguage(array $fields, ?string $language = null): array - { - $kirby = App::instance(null, true); + if ($language === null) { + $language = $kirby->language()->code(); + } - // only modify the fields if we have a valid Kirby multilang instance - if (!$kirby || $kirby->multilang() === false) { - return $fields; - } + if ($language !== $kirby->defaultLanguage()->code()) { + foreach ($fields as $fieldName => $fieldProps) { + // switch untranslatable fields to readonly + if (($fieldProps['translate'] ?? true) === false) { + $fields[$fieldName]['unset'] = true; + $fields[$fieldName]['disabled'] = true; + } + } + } - if ($language === null) { - $language = $kirby->language()->code(); - } + return $fields; + } - if ($language !== $kirby->defaultLanguage()->code()) { - foreach ($fields as $fieldName => $fieldProps) { - // switch untranslatable fields to readonly - if (($fieldProps['translate'] ?? true) === false) { - $fields[$fieldName]['unset'] = true; - $fields[$fieldName]['disabled'] = true; - } - } - } + /** + * Converts the data of fields to strings + * + * @param false $defaults + * @return array + */ + public function strings($defaults = false): array + { + $strings = []; - return $fields; - } + foreach ($this->data($defaults) as $key => $value) { + if ($value === null) { + $strings[$key] = null; + } elseif (is_array($value) === true) { + $strings[$key] = Data::encode($value, 'yaml'); + } else { + $strings[$key] = $value; + } + } - /** - * Converts the data of fields to strings - * - * @param false $defaults - * @return array - */ - public function strings($defaults = false): array - { - $strings = []; + return $strings; + } - foreach ($this->data($defaults) as $key => $value) { - if ($value === null) { - $strings[$key] = null; - } elseif (is_array($value) === true) { - $strings[$key] = Data::encode($value, 'yaml'); - } else { - $strings[$key] = $value; - } - } + /** + * Converts the form to a plain array + * + * @return array + */ + public function toArray(): array + { + $array = [ + 'errors' => $this->errors(), + 'fields' => $this->fields->toArray(fn ($item) => $item->toArray()), + 'invalid' => $this->isInvalid() + ]; - return $strings; - } + return $array; + } - /** - * Converts the form to a plain array - * - * @return array - */ - public function toArray(): array - { - $array = [ - 'errors' => $this->errors(), - 'fields' => $this->fields->toArray(fn ($item) => $item->toArray()), - 'invalid' => $this->isInvalid() - ]; - - return $array; - } - - /** - * Returns form values - * - * @return array - */ - public function values(): array - { - return $this->values; - } + /** + * Returns form values + * + * @return array + */ + public function values(): array + { + return $this->values; + } } diff --git a/kirby/src/Form/Mixin/EmptyState.php b/kirby/src/Form/Mixin/EmptyState.php index 14b2c36..782acaa 100644 --- a/kirby/src/Form/Mixin/EmptyState.php +++ b/kirby/src/Form/Mixin/EmptyState.php @@ -4,15 +4,15 @@ namespace Kirby\Form\Mixin; trait EmptyState { - protected $empty; + protected $empty; - protected function setEmpty($empty = null) - { - $this->empty = $this->i18n($empty); - } + protected function setEmpty($empty = null) + { + $this->empty = $this->i18n($empty); + } - public function empty(): ?string - { - return $this->stringTemplate($this->empty); - } + public function empty(): ?string + { + return $this->stringTemplate($this->empty); + } } diff --git a/kirby/src/Form/Mixin/Max.php b/kirby/src/Form/Mixin/Max.php index 42b9ffb..b02825e 100644 --- a/kirby/src/Form/Mixin/Max.php +++ b/kirby/src/Form/Mixin/Max.php @@ -4,15 +4,15 @@ namespace Kirby\Form\Mixin; trait Max { - protected $max; + protected $max; - public function max(): ?int - { - return $this->max; - } + public function max(): ?int + { + return $this->max; + } - protected function setMax(int $max = null) - { - $this->max = $max; - } + protected function setMax(int $max = null) + { + $this->max = $max; + } } diff --git a/kirby/src/Form/Mixin/Min.php b/kirby/src/Form/Mixin/Min.php index 7bf6585..46a8d87 100644 --- a/kirby/src/Form/Mixin/Min.php +++ b/kirby/src/Form/Mixin/Min.php @@ -4,15 +4,15 @@ namespace Kirby\Form\Mixin; trait Min { - protected $min; + protected $min; - public function min(): ?int - { - return $this->min; - } + public function min(): ?int + { + return $this->min; + } - protected function setMin(int $min = null) - { - $this->min = $min; - } + protected function setMin(int $min = null) + { + $this->min = $min; + } } diff --git a/kirby/src/Form/Options.php b/kirby/src/Form/Options.php index fbc6a1f..4873b88 100644 --- a/kirby/src/Form/Options.php +++ b/kirby/src/Form/Options.php @@ -19,189 +19,192 @@ use Kirby\Toolkit\I18n; */ class Options { - /** - * Returns the classes of predefined Kirby objects - * - * @return array - */ - protected static function aliases(): array - { - return [ - 'Kirby\Cms\File' => 'file', - 'Kirby\Toolkit\Obj' => 'arrayItem', - 'Kirby\Cms\Block' => 'block', - 'Kirby\Cms\Page' => 'page', - 'Kirby\Cms\StructureObject' => 'structureItem', - 'Kirby\Cms\User' => 'user', - ]; - } + /** + * Returns the classes of predefined Kirby objects + * + * @return array + */ + protected static function aliases(): array + { + return [ + 'Kirby\Cms\File' => 'file', + 'Kirby\Toolkit\Obj' => 'arrayItem', + 'Kirby\Cms\Block' => 'block', + 'Kirby\Cms\Page' => 'page', + 'Kirby\Cms\StructureObject' => 'structureItem', + 'Kirby\Cms\User' => 'user', + ]; + } - /** - * Brings options through api - * - * @param $api - * @param \Kirby\Cms\Model|null $model - * @return array - */ - public static function api($api, $model = null): array - { - $model ??= App::instance()->site(); - $fetch = null; - $text = null; - $value = null; + /** + * Brings options through api + * + * @param $api + * @param \Kirby\Cms\Model|null $model + * @return array + */ + public static function api($api, $model = null): array + { + $model ??= App::instance()->site(); + $fetch = null; + $text = null; + $value = null; - if (is_array($api) === true) { - $fetch = $api['fetch'] ?? null; - $text = $api['text'] ?? null; - $value = $api['value'] ?? null; - $url = $api['url'] ?? null; - } else { - $url = $api; - } + if (is_array($api) === true) { + $fetch = $api['fetch'] ?? null; + $text = $api['text'] ?? null; + $value = $api['value'] ?? null; + $url = $api['url'] ?? null; + } else { + $url = $api; + } - $optionsApi = new OptionsApi([ - 'data' => static::data($model), - 'fetch' => $fetch, - 'url' => $url, - 'text' => $text, - 'value' => $value - ]); + $optionsApi = new OptionsApi([ + 'data' => static::data($model), + 'fetch' => $fetch, + 'url' => $url, + 'text' => $text, + 'value' => $value + ]); - return $optionsApi->options(); - } + return $optionsApi->options(); + } - /** - * @param \Kirby\Cms\Model $model - * @return array - */ - protected static function data($model): array - { - $kirby = $model->kirby(); + /** + * @param \Kirby\Cms\Model $model + * @return array + */ + protected static function data($model): array + { + $kirby = $model->kirby(); - // default data setup - $data = [ - 'kirby' => $kirby, - 'site' => $kirby->site(), - 'users' => $kirby->users(), - ]; + // default data setup + $data = [ + 'kirby' => $kirby, + 'site' => $kirby->site(), + 'users' => $kirby->users(), + ]; - // add the model by the proper alias - foreach (static::aliases() as $className => $alias) { - if (is_a($model, $className) === true) { - $data[$alias] = $model; - } - } + // add the model by the proper alias + foreach (static::aliases() as $className => $alias) { + if (is_a($model, $className) === true) { + $data[$alias] = $model; + } + } - return $data; - } + return $data; + } - /** - * Brings options by supporting both api and query - * - * @param $options - * @param array $props - * @param \Kirby\Cms\Model|null $model - * @return array - */ - public static function factory($options, array $props = [], $model = null): array - { - switch ($options) { - case 'api': - $options = static::api($props['api'], $model); - break; - case 'query': - $options = static::query($props['query'], $model); - break; - case 'children': - case 'grandChildren': - case 'siblings': - case 'index': - case 'files': - case 'images': - case 'documents': - case 'videos': - case 'audio': - case 'code': - case 'archives': - $options = static::query('page.' . $options, $model); - break; - case 'pages': - $options = static::query('site.index', $model); - break; - } + /** + * Brings options by supporting both api and query + * + * @param $options + * @param array $props + * @param \Kirby\Cms\Model|null $model + * @return array + */ + public static function factory($options, array $props = [], $model = null): array + { + switch ($options) { + case 'api': + $options = static::api($props['api'], $model); + break; + case 'query': + $options = static::query($props['query'], $model); + break; + case 'children': + case 'grandChildren': + case 'siblings': + case 'index': + case 'files': + case 'images': + case 'documents': + case 'videos': + case 'audio': + case 'code': + case 'archives': + $options = static::query('page.' . $options, $model); + break; + case 'pages': + $options = static::query('site.index', $model); + break; + } - if (is_array($options) === false) { - return []; - } + if (is_array($options) === false) { + return []; + } - $result = []; + $result = []; - foreach ($options as $key => $option) { - if (is_array($option) === false || isset($option['value']) === false) { - $option = [ - 'value' => is_int($key) ? $option : $key, - 'text' => $option - ]; - } + foreach ($options as $key => $option) { + if (is_array($option) === false || isset($option['value']) === false) { + $option = [ + 'value' => is_int($key) ? $option : $key, + 'text' => $option + ]; + } - // translate the option text - if (is_array($option['text']) === true) { - $option['text'] = I18n::translate($option['text'], $option['text']); - } + // fallback for the text + $option['text'] ??= $option['value']; - // add the option to the list - $result[] = $option; - } + // translate the option text + if (is_array($option['text']) === true) { + $option['text'] = I18n::translate($option['text'], $option['text']); + } - return $result; - } + // add the option to the list + $result[] = $option; + } - /** - * Brings options with query - * - * @param $query - * @param \Kirby\Cms\Model|null $model - * @return array - */ - public static function query($query, $model = null): array - { - $model ??= App::instance()->site(); + return $result; + } - // default text setup - $text = [ - 'arrayItem' => '{{ arrayItem.value }}', - 'block' => '{{ block.type }}: {{ block.id }}', - 'file' => '{{ file.filename }}', - 'page' => '{{ page.title }}', - 'structureItem' => '{{ structureItem.title }}', - 'user' => '{{ user.username }}', - ]; + /** + * Brings options with query + * + * @param $query + * @param \Kirby\Cms\Model|null $model + * @return array + */ + public static function query($query, $model = null): array + { + $model ??= App::instance()->site(); - // default value setup - $value = [ - 'arrayItem' => '{{ arrayItem.value }}', - 'block' => '{{ block.id }}', - 'file' => '{{ file.id }}', - 'page' => '{{ page.id }}', - 'structureItem' => '{{ structureItem.id }}', - 'user' => '{{ user.email }}', - ]; + // default text setup + $text = [ + 'arrayItem' => '{{ arrayItem.value }}', + 'block' => '{{ block.type }}: {{ block.id }}', + 'file' => '{{ file.filename }}', + 'page' => '{{ page.title }}', + 'structureItem' => '{{ structureItem.title }}', + 'user' => '{{ user.username }}', + ]; - // resolve array query setup - if (is_array($query) === true) { - $text = $query['text'] ?? $text; - $value = $query['value'] ?? $value; - $query = $query['fetch'] ?? null; - } + // default value setup + $value = [ + 'arrayItem' => '{{ arrayItem.value }}', + 'block' => '{{ block.id }}', + 'file' => '{{ file.id }}', + 'page' => '{{ page.id }}', + 'structureItem' => '{{ structureItem.id }}', + 'user' => '{{ user.email }}', + ]; - $optionsQuery = new OptionsQuery([ - 'aliases' => static::aliases(), - 'data' => static::data($model), - 'query' => $query, - 'text' => $text, - 'value' => $value - ]); + // resolve array query setup + if (is_array($query) === true) { + $text = $query['text'] ?? $text; + $value = $query['value'] ?? $value; + $query = $query['fetch'] ?? null; + } - return $optionsQuery->options(); - } + $optionsQuery = new OptionsQuery([ + 'aliases' => static::aliases(), + 'data' => static::data($model), + 'query' => $query, + 'text' => $text, + 'value' => $value + ]); + + return $optionsQuery->options(); + } } diff --git a/kirby/src/Form/OptionsApi.php b/kirby/src/Form/OptionsApi.php index 826e6f6..c4df0b4 100644 --- a/kirby/src/Form/OptionsApi.php +++ b/kirby/src/Form/OptionsApi.php @@ -23,220 +23,220 @@ use Kirby\Toolkit\Str; */ class OptionsApi { - use Properties; + use Properties; - /** - * @var array - */ - protected $data; + /** + * @var array + */ + protected $data; - /** - * @var string|null - */ - protected $fetch; + /** + * @var string|null + */ + protected $fetch; - /** - * @var array|string|null - */ - protected $options; + /** + * @var array|string|null + */ + protected $options; - /** - * @var string - */ - protected $text = '{{ item.value }}'; + /** + * @var string + */ + protected $text = '{{ item.value }}'; - /** - * @var string - */ - protected $url; + /** + * @var string + */ + protected $url; - /** - * @var string - */ - protected $value = '{{ item.key }}'; + /** + * @var string + */ + protected $value = '{{ item.key }}'; - /** - * OptionsApi constructor - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setProperties($props); - } + /** + * OptionsApi constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } - /** - * @return array - */ - public function data(): array - { - return $this->data; - } + /** + * @return array + */ + public function data(): array + { + return $this->data; + } - /** - * @return mixed - */ - public function fetch() - { - return $this->fetch; - } + /** + * @return mixed + */ + public function fetch() + { + return $this->fetch; + } - /** - * @param string $field - * @param array $data - * @return string - */ - protected function field(string $field, array $data): string - { - $value = $this->$field(); - return Str::safeTemplate($value, $data); - } + /** + * @param string $field + * @param array $data + * @return string + */ + protected function field(string $field, array $data): string + { + $value = $this->$field(); + return Str::safeTemplate($value, $data); + } - /** - * @return array - * @throws \Exception - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function options(): array - { - if (is_array($this->options) === true) { - return $this->options; - } + /** + * @return array + * @throws \Exception + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function options(): array + { + if (is_array($this->options) === true) { + return $this->options; + } - if (Url::isAbsolute($this->url()) === true) { - // URL, request via cURL - $data = Remote::get($this->url())->json(); - } else { - // local file, get contents locally + if (Url::isAbsolute($this->url()) === true) { + // URL, request via cURL + $data = Remote::get($this->url())->json(); + } else { + // local file, get contents locally - // ensure the file exists before trying to load it as the - // file_get_contents() warnings need to be suppressed - if (is_file($this->url()) !== true) { - throw new Exception('Local file ' . $this->url() . ' was not found'); - } + // ensure the file exists before trying to load it as the + // file_get_contents() warnings need to be suppressed + if (is_file($this->url()) !== true) { + throw new Exception('Local file ' . $this->url() . ' was not found'); + } - $content = @file_get_contents($this->url()); + $content = @file_get_contents($this->url()); - if (is_string($content) !== true) { - throw new Exception('Unexpected read error'); // @codeCoverageIgnore - } + if (is_string($content) !== true) { + throw new Exception('Unexpected read error'); // @codeCoverageIgnore + } - if (empty($content) === true) { - return []; - } + if (empty($content) === true) { + return []; + } - $data = json_decode($content, true); - } + $data = json_decode($content, true); + } - if (is_array($data) === false) { - throw new InvalidArgumentException('Invalid options format'); - } + if (is_array($data) === false) { + throw new InvalidArgumentException('Invalid options format'); + } - $result = (new Query($this->fetch(), Nest::create($data)))->result(); - $options = []; + $result = (new Query($this->fetch(), Nest::create($data)))->result(); + $options = []; - foreach ($result as $item) { - $data = array_merge($this->data(), ['item' => $item]); + foreach ($result as $item) { + $data = array_merge($this->data(), ['item' => $item]); - $options[] = [ - 'text' => $this->field('text', $data), - 'value' => $this->field('value', $data), - ]; - } + $options[] = [ + 'text' => $this->field('text', $data), + 'value' => $this->field('value', $data), + ]; + } - return $options; - } + return $options; + } - /** - * @param array $data - * @return $this - */ - protected function setData(array $data) - { - $this->data = $data; - return $this; - } + /** + * @param array $data + * @return $this + */ + protected function setData(array $data) + { + $this->data = $data; + return $this; + } - /** - * @param string|null $fetch - * @return $this - */ - protected function setFetch(?string $fetch = null) - { - $this->fetch = $fetch; - return $this; - } + /** + * @param string|null $fetch + * @return $this + */ + protected function setFetch(?string $fetch = null) + { + $this->fetch = $fetch; + return $this; + } - /** - * @param array|string|null $options - * @return $this - */ - protected function setOptions($options = null) - { - $this->options = $options; - return $this; - } + /** + * @param array|string|null $options + * @return $this + */ + protected function setOptions($options = null) + { + $this->options = $options; + return $this; + } - /** - * @param string $text - * @return $this - */ - protected function setText(?string $text = null) - { - $this->text = $text; - return $this; - } + /** + * @param string $text + * @return $this + */ + protected function setText(?string $text = null) + { + $this->text = $text; + return $this; + } - /** - * @param string $url - * @return $this - */ - protected function setUrl(string $url) - { - $this->url = $url; - return $this; - } + /** + * @param string $url + * @return $this + */ + protected function setUrl(string $url) + { + $this->url = $url; + return $this; + } - /** - * @param string|null $value - * @return $this - */ - protected function setValue(?string $value = null) - { - $this->value = $value; - return $this; - } + /** + * @param string|null $value + * @return $this + */ + protected function setValue(?string $value = null) + { + $this->value = $value; + return $this; + } - /** - * @return string - */ - public function text(): string - { - return $this->text; - } + /** + * @return string + */ + public function text(): string + { + return $this->text; + } - /** - * @return array - * @throws \Kirby\Exception\InvalidArgumentException - */ - public function toArray(): array - { - return $this->options(); - } + /** + * @return array + * @throws \Kirby\Exception\InvalidArgumentException + */ + public function toArray(): array + { + return $this->options(); + } - /** - * @return string - */ - public function url(): string - { - return Str::template($this->url, $this->data()); - } + /** + * @return string + */ + public function url(): string + { + return Str::template($this->url, $this->data()); + } - /** - * @return string - */ - public function value(): string - { - return $this->value; - } + /** + * @return string + */ + public function value(): string + { + return $this->value; + } } diff --git a/kirby/src/Form/OptionsQuery.php b/kirby/src/Form/OptionsQuery.php index cc2a620..c91ac64 100644 --- a/kirby/src/Form/OptionsQuery.php +++ b/kirby/src/Form/OptionsQuery.php @@ -25,247 +25,247 @@ use Kirby\Toolkit\Str; */ class OptionsQuery { - use Properties; + use Properties; - /** - * @var array - */ - protected $aliases = []; + /** + * @var array + */ + protected $aliases = []; - /** - * @var array - */ - protected $data; + /** + * @var array + */ + protected $data; - /** - * @var array|string|null - */ - protected $options; + /** + * @var array|string|null + */ + protected $options; - /** - * @var string - */ - protected $query; + /** + * @var string + */ + protected $query; - /** - * @var mixed - */ - protected $text; + /** + * @var mixed + */ + protected $text; - /** - * @var mixed - */ - protected $value; + /** + * @var mixed + */ + protected $value; - /** - * OptionsQuery constructor - * - * @param array $props - */ - public function __construct(array $props) - { - $this->setProperties($props); - } + /** + * OptionsQuery constructor + * + * @param array $props + */ + public function __construct(array $props) + { + $this->setProperties($props); + } - /** - * @return array - */ - public function aliases(): array - { - return $this->aliases; - } + /** + * @return array + */ + public function aliases(): array + { + return $this->aliases; + } - /** - * @return array - */ - public function data(): array - { - return $this->data; - } + /** + * @return array + */ + public function data(): array + { + return $this->data; + } - /** - * @param string $object - * @param string $field - * @param array $data - * @return string - * @throws \Kirby\Exception\NotFoundException - */ - protected function template(string $object, string $field, array $data) - { - $value = $this->$field(); + /** + * @param string $object + * @param string $field + * @param array $data + * @return string + * @throws \Kirby\Exception\NotFoundException + */ + protected function template(string $object, string $field, array $data) + { + $value = $this->$field(); - if (is_array($value) === true) { - if (isset($value[$object]) === false) { - throw new NotFoundException('Missing "' . $field . '" definition'); - } + if (is_array($value) === true) { + if (isset($value[$object]) === false) { + throw new NotFoundException('Missing "' . $field . '" definition'); + } - $value = $value[$object]; - } + $value = $value[$object]; + } - return Str::safeTemplate($value, $data); - } + return Str::safeTemplate($value, $data); + } - /** - * @return array - */ - public function options(): array - { - if (is_array($this->options) === true) { - return $this->options; - } + /** + * @return array + */ + public function options(): array + { + if (is_array($this->options) === true) { + return $this->options; + } - $data = $this->data(); - $query = new Query($this->query(), $data); - $result = $query->result(); - $result = $this->resultToCollection($result); - $options = []; + $data = $this->data(); + $query = new Query($this->query(), $data); + $result = $query->result(); + $result = $this->resultToCollection($result); + $options = []; - foreach ($result as $item) { - $alias = $this->resolve($item); - $data = array_merge($data, [$alias => $item]); + foreach ($result as $item) { + $alias = $this->resolve($item); + $data = array_merge($data, [$alias => $item]); - $options[] = [ - 'text' => $this->template($alias, 'text', $data), - 'value' => $this->template($alias, 'value', $data) - ]; - } + $options[] = [ + 'text' => $this->template($alias, 'text', $data), + 'value' => $this->template($alias, 'value', $data) + ]; + } - return $this->options = $options; - } + return $this->options = $options; + } - /** - * @return string - */ - public function query(): string - { - return $this->query; - } + /** + * @return string + */ + public function query(): string + { + return $this->query; + } - /** - * @param $object - * @return mixed|string|null - */ - public function resolve($object) - { - // fast access - if ($alias = ($this->aliases[get_class($object)] ?? null)) { - return $alias; - } + /** + * @param $object + * @return mixed|string|null + */ + public function resolve($object) + { + // fast access + if ($alias = ($this->aliases[get_class($object)] ?? null)) { + return $alias; + } - // slow but precise resolving - foreach ($this->aliases as $className => $alias) { - if (is_a($object, $className) === true) { - return $alias; - } - } + // slow but precise resolving + foreach ($this->aliases as $className => $alias) { + if (is_a($object, $className) === true) { + return $alias; + } + } - return 'item'; - } + return 'item'; + } - /** - * @param $result - * @throws \Kirby\Exception\InvalidArgumentException - */ - protected function resultToCollection($result) - { - if (is_array($result)) { - foreach ($result as $key => $item) { - if (is_scalar($item) === true) { - $result[$key] = new Obj([ - 'key' => new Field(null, 'key', $key), - 'value' => new Field(null, 'value', $item), - ]); - } - } + /** + * @param $result + * @throws \Kirby\Exception\InvalidArgumentException + */ + protected function resultToCollection($result) + { + if (is_array($result)) { + foreach ($result as $key => $item) { + if (is_scalar($item) === true) { + $result[$key] = new Obj([ + 'key' => new Field(null, 'key', $key), + 'value' => new Field(null, 'value', $item), + ]); + } + } - $result = new Collection($result); - } + $result = new Collection($result); + } - if (is_a($result, 'Kirby\Toolkit\Collection') === false) { - throw new InvalidArgumentException('Invalid query result data'); - } + if (is_a($result, 'Kirby\Toolkit\Collection') === false) { + throw new InvalidArgumentException('Invalid query result data'); + } - return $result; - } + return $result; + } - /** - * @param array|null $aliases - * @return $this - */ - protected function setAliases(?array $aliases = null) - { - $this->aliases = $aliases; - return $this; - } + /** + * @param array|null $aliases + * @return $this + */ + protected function setAliases(?array $aliases = null) + { + $this->aliases = $aliases; + return $this; + } - /** - * @param array $data - * @return $this - */ - protected function setData(array $data) - { - $this->data = $data; - return $this; - } + /** + * @param array $data + * @return $this + */ + protected function setData(array $data) + { + $this->data = $data; + return $this; + } - /** - * @param array|string|null $options - * @return $this - */ - protected function setOptions($options = null) - { - $this->options = $options; - return $this; - } + /** + * @param array|string|null $options + * @return $this + */ + protected function setOptions($options = null) + { + $this->options = $options; + return $this; + } - /** - * @param string $query - * @return $this - */ - protected function setQuery(string $query) - { - $this->query = $query; - return $this; - } + /** + * @param string $query + * @return $this + */ + protected function setQuery(string $query) + { + $this->query = $query; + return $this; + } - /** - * @param mixed $text - * @return $this - */ - protected function setText($text) - { - $this->text = $text; - return $this; - } + /** + * @param mixed $text + * @return $this + */ + protected function setText($text) + { + $this->text = $text; + return $this; + } - /** - * @param mixed $value - * @return $this - */ - protected function setValue($value) - { - $this->value = $value; - return $this; - } + /** + * @param mixed $value + * @return $this + */ + protected function setValue($value) + { + $this->value = $value; + return $this; + } - /** - * @return mixed - */ - public function text() - { - return $this->text; - } + /** + * @return mixed + */ + public function text() + { + return $this->text; + } - public function toArray(): array - { - return $this->options(); - } + public function toArray(): array + { + return $this->options(); + } - /** - * @return mixed - */ - public function value() - { - return $this->value; - } + /** + * @return mixed + */ + public function value() + { + return $this->value; + } } diff --git a/kirby/src/Form/Validations.php b/kirby/src/Form/Validations.php index c6c5053..5834624 100644 --- a/kirby/src/Form/Validations.php +++ b/kirby/src/Form/Validations.php @@ -16,279 +16,279 @@ use Kirby\Toolkit\V; */ class Validations { - /** - * Validates if the field value is boolean - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function boolean($field, $value): bool - { - if ($field->isEmpty($value) === false) { - if (is_bool($value) === false) { - throw new InvalidArgumentException([ - 'key' => 'validation.boolean' - ]); - } - } + /** + * Validates if the field value is boolean + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function boolean($field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (is_bool($value) === false) { + throw new InvalidArgumentException([ + 'key' => 'validation.boolean' + ]); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is valid date - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function date($field, $value): bool - { - if ($field->isEmpty($value) === false) { - if (V::date($value) !== true) { - throw new InvalidArgumentException( - V::message('date', $value) - ); - } - } + /** + * Validates if the field value is valid date + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function date($field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::date($value) !== true) { + throw new InvalidArgumentException( + V::message('date', $value) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is valid email - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function email($field, $value): bool - { - if ($field->isEmpty($value) === false) { - if (V::email($value) === false) { - throw new InvalidArgumentException( - V::message('email', $value) - ); - } - } + /** + * Validates if the field value is valid email + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function email($field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::email($value) === false) { + throw new InvalidArgumentException( + V::message('email', $value) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is maximum - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function max($field, $value): bool - { - if ($field->isEmpty($value) === false && $field->max() !== null) { - if (V::max($value, $field->max()) === false) { - throw new InvalidArgumentException( - V::message('max', $value, $field->max()) - ); - } - } + /** + * Validates if the field value is maximum + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function max($field, $value): bool + { + if ($field->isEmpty($value) === false && $field->max() !== null) { + if (V::max($value, $field->max()) === false) { + throw new InvalidArgumentException( + V::message('max', $value, $field->max()) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is max length - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function maxlength($field, $value): bool - { - if ($field->isEmpty($value) === false && $field->maxlength() !== null) { - if (V::maxLength($value, $field->maxlength()) === false) { - throw new InvalidArgumentException( - V::message('maxlength', $value, $field->maxlength()) - ); - } - } + /** + * Validates if the field value is max length + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function maxlength($field, $value): bool + { + if ($field->isEmpty($value) === false && $field->maxlength() !== null) { + if (V::maxLength($value, $field->maxlength()) === false) { + throw new InvalidArgumentException( + V::message('maxlength', $value, $field->maxlength()) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is minimum - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function min($field, $value): bool - { - if ($field->isEmpty($value) === false && $field->min() !== null) { - if (V::min($value, $field->min()) === false) { - throw new InvalidArgumentException( - V::message('min', $value, $field->min()) - ); - } - } + /** + * Validates if the field value is minimum + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function min($field, $value): bool + { + if ($field->isEmpty($value) === false && $field->min() !== null) { + if (V::min($value, $field->min()) === false) { + throw new InvalidArgumentException( + V::message('min', $value, $field->min()) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is min length - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function minlength($field, $value): bool - { - if ($field->isEmpty($value) === false && $field->minlength() !== null) { - if (V::minLength($value, $field->minlength()) === false) { - throw new InvalidArgumentException( - V::message('minlength', $value, $field->minlength()) - ); - } - } + /** + * Validates if the field value is min length + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function minlength($field, $value): bool + { + if ($field->isEmpty($value) === false && $field->minlength() !== null) { + if (V::minLength($value, $field->minlength()) === false) { + throw new InvalidArgumentException( + V::message('minlength', $value, $field->minlength()) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value matches defined pattern - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function pattern($field, $value): bool - { - if ($field->isEmpty($value) === false && $field->pattern() !== null) { - if (V::match($value, '/' . $field->pattern() . '/i') === false) { - throw new InvalidArgumentException( - V::message('match') - ); - } - } + /** + * Validates if the field value matches defined pattern + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function pattern($field, $value): bool + { + if ($field->isEmpty($value) === false && $field->pattern() !== null) { + if (V::match($value, '/' . $field->pattern() . '/i') === false) { + throw new InvalidArgumentException( + V::message('match') + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is required - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function required($field, $value): bool - { - if ($field->isRequired() === true && $field->save() === true && $field->isEmpty($value) === true) { - throw new InvalidArgumentException([ - 'key' => 'validation.required' - ]); - } + /** + * Validates if the field value is required + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function required($field, $value): bool + { + if ($field->isRequired() === true && $field->save() === true && $field->isEmpty($value) === true) { + throw new InvalidArgumentException([ + 'key' => 'validation.required' + ]); + } - return true; - } + return true; + } - /** - * Validates if the field value is in defined options - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function option($field, $value): bool - { - if ($field->isEmpty($value) === false) { - $values = array_column($field->options(), 'value'); + /** + * Validates if the field value is in defined options + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function option($field, $value): bool + { + if ($field->isEmpty($value) === false) { + $values = array_column($field->options(), 'value'); - if (in_array($value, $values, true) !== true) { - throw new InvalidArgumentException([ - 'key' => 'validation.option' - ]); - } - } + if (in_array($value, $values, true) !== true) { + throw new InvalidArgumentException([ + 'key' => 'validation.option' + ]); + } + } - return true; - } + return true; + } - /** - * Validates if the field values is in defined options - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function options($field, $value): bool - { - if ($field->isEmpty($value) === false) { - $values = array_column($field->options(), 'value'); - foreach ($value as $val) { - if (in_array($val, $values, true) === false) { - throw new InvalidArgumentException([ - 'key' => 'validation.option' - ]); - } - } - } + /** + * Validates if the field values is in defined options + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function options($field, $value): bool + { + if ($field->isEmpty($value) === false) { + $values = array_column($field->options(), 'value'); + foreach ($value as $val) { + if (in_array($val, $values, true) === false) { + throw new InvalidArgumentException([ + 'key' => 'validation.option' + ]); + } + } + } - return true; - } + return true; + } - /** - * Validates if the field value is valid time - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function time($field, $value): bool - { - if ($field->isEmpty($value) === false) { - if (V::time($value) !== true) { - throw new InvalidArgumentException( - V::message('time', $value) - ); - } - } + /** + * Validates if the field value is valid time + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function time($field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::time($value) !== true) { + throw new InvalidArgumentException( + V::message('time', $value) + ); + } + } - return true; - } + return true; + } - /** - * Validates if the field value is valid url - * - * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field - * @param $value - * @return bool - * @throws \Kirby\Exception\InvalidArgumentException - */ - public static function url($field, $value): bool - { - if ($field->isEmpty($value) === false) { - if (V::url($value) === false) { - throw new InvalidArgumentException( - V::message('url', $value) - ); - } - } + /** + * Validates if the field value is valid url + * + * @param \Kirby\Form\Field|\Kirby\Form\FieldClass $field + * @param $value + * @return bool + * @throws \Kirby\Exception\InvalidArgumentException + */ + public static function url($field, $value): bool + { + if ($field->isEmpty($value) === false) { + if (V::url($value) === false) { + throw new InvalidArgumentException( + V::message('url', $value) + ); + } + } - return true; - } + return true; + } } diff --git a/kirby/src/Http/Cookie.php b/kirby/src/Http/Cookie.php index d076718..b63c606 100644 --- a/kirby/src/Http/Cookie.php +++ b/kirby/src/Http/Cookie.php @@ -2,6 +2,7 @@ namespace Kirby\Http; +use Kirby\Cms\App; use Kirby\Toolkit\Str; /** @@ -16,194 +17,221 @@ use Kirby\Toolkit\Str; */ class Cookie { - /** - * Key to use for cookie signing - * @var string - */ - public static $key = 'KirbyHttpCookieKey'; + /** + * Key to use for cookie signing + * @var string + */ + public static $key = 'KirbyHttpCookieKey'; - /** - * Set a new cookie - * - * - * - * cookie::set('mycookie', 'hello', ['lifetime' => 60]); - * // expires in 1 hour - * - * - * - * @param string $key The name of the cookie - * @param string $value The cookie content - * @param array $options Array of options: - * lifetime, path, domain, secure, httpOnly, sameSite - * @return bool true: cookie was created, - * false: cookie creation failed - */ - public static function set(string $key, string $value, array $options = []): bool - { - // extract options - $expires = static::lifetime($options['lifetime'] ?? 0); - $path = $options['path'] ?? '/'; - $domain = $options['domain'] ?? null; - $secure = $options['secure'] ?? false; - $httponly = $options['httpOnly'] ?? true; - $samesite = $options['sameSite'] ?? 'Lax'; + /** + * Set a new cookie + * + * + * + * cookie::set('mycookie', 'hello', ['lifetime' => 60]); + * // expires in 1 hour + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param array $options Array of options: + * lifetime, path, domain, secure, httpOnly, sameSite + * @return bool true: cookie was created, + * false: cookie creation failed + */ + public static function set(string $key, string $value, array $options = []): bool + { + // modify CMS caching behavior + static::trackUsage($key); - // add an HMAC signature of the value - $value = static::hmac($value) . '+' . $value; + // extract options + $expires = static::lifetime($options['lifetime'] ?? 0); + $path = $options['path'] ?? '/'; + $domain = $options['domain'] ?? null; + $secure = $options['secure'] ?? false; + $httponly = $options['httpOnly'] ?? true; + $samesite = $options['sameSite'] ?? 'Lax'; - // store that thing in the cookie global - $_COOKIE[$key] = $value; + // add an HMAC signature of the value + $value = static::hmac($value) . '+' . $value; - // store the cookie - $options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite'); - return setcookie($key, $value, $options); - } + // store that thing in the cookie global + $_COOKIE[$key] = $value; - /** - * Calculates the lifetime for a cookie - * - * @param int $minutes Number of minutes or timestamp - * @return int - */ - public static function lifetime(int $minutes): int - { - if ($minutes > 1000000000) { - // absolute timestamp - return $minutes; - } elseif ($minutes > 0) { - // minutes from now - return time() + ($minutes * 60); - } else { - return 0; - } - } + // store the cookie + $options = compact('expires', 'path', 'domain', 'secure', 'httponly', 'samesite'); + return setcookie($key, $value, $options); + } - /** - * Stores a cookie forever - * - * - * - * cookie::forever('mycookie', 'hello'); - * // never expires - * - * - * - * @param string $key The name of the cookie - * @param string $value The cookie content - * @param array $options Array of options: - * path, domain, secure, httpOnly - * @return bool true: cookie was created, - * false: cookie creation failed - */ - public static function forever(string $key, string $value, array $options = []): bool - { - // 9999-12-31 if supported (lower on 32-bit servers) - $options['lifetime'] = min(253402214400, PHP_INT_MAX); - return static::set($key, $value, $options); - } + /** + * Calculates the lifetime for a cookie + * + * @param int $minutes Number of minutes or timestamp + * @return int + */ + public static function lifetime(int $minutes): int + { + if ($minutes > 1000000000) { + // absolute timestamp + return $minutes; + } elseif ($minutes > 0) { + // minutes from now + return time() + ($minutes * 60); + } else { + return 0; + } + } - /** - * Get a cookie value - * - * - * - * cookie::get('mycookie', 'peter'); - * // sample output: 'hello' or if the cookie is not set 'peter' - * - * - * - * @param string|null $key The name of the cookie - * @param string|null $default The default value, which should be returned - * if the cookie has not been found - * @return mixed The found value - */ - public static function get(string $key = null, string $default = null) - { - if ($key === null) { - return $_COOKIE; - } - $value = $_COOKIE[$key] ?? null; - return empty($value) ? $default : static::parse($value); - } + /** + * Stores a cookie forever + * + * + * + * cookie::forever('mycookie', 'hello'); + * // never expires + * + * + * + * @param string $key The name of the cookie + * @param string $value The cookie content + * @param array $options Array of options: + * path, domain, secure, httpOnly + * @return bool true: cookie was created, + * false: cookie creation failed + */ + public static function forever(string $key, string $value, array $options = []): bool + { + // 9999-12-31 if supported (lower on 32-bit servers) + $options['lifetime'] = min(253402214400, PHP_INT_MAX); + return static::set($key, $value, $options); + } - /** - * Checks if a cookie exists - * - * @param string $key - * @return bool - */ - public static function exists(string $key): bool - { - return static::get($key) !== null; - } + /** + * Get a cookie value + * + * + * + * cookie::get('mycookie', 'peter'); + * // sample output: 'hello' or if the cookie is not set 'peter' + * + * + * + * @param string|null $key The name of the cookie + * @param string|null $default The default value, which should be returned + * if the cookie has not been found + * @return mixed The found value + */ + public static function get(string $key = null, string $default = null) + { + if ($key === null) { + return $_COOKIE; + } - /** - * Creates a HMAC for the cookie value - * Used as a cookie signature to prevent easy tampering with cookie data - * - * @param string $value - * @return string - */ - protected static function hmac(string $value): string - { - return hash_hmac('sha1', $value, static::$key); - } + // modify CMS caching behavior + static::trackUsage($key); - /** - * Parses the hashed value from a cookie - * and tries to extract the value - * - * @param string $string - * @return mixed - */ - protected static function parse(string $string) - { - // if no hash-value separator is present, we can't parse the value - if (strpos($string, '+') === false) { - return null; - } + $value = $_COOKIE[$key] ?? null; + return empty($value) ? $default : static::parse($value); + } - // extract hash and value - $hash = Str::before($string, '+'); - $value = Str::after($string, '+'); + /** + * Checks if a cookie exists + * + * @param string $key + * @return bool + */ + public static function exists(string $key): bool + { + return static::get($key) !== null; + } - // if the hash or the value is missing at all return null - // $value can be an empty string, $hash can't be! - if (!is_string($hash) || $hash === '' || !is_string($value)) { - return null; - } + /** + * Creates a HMAC for the cookie value + * Used as a cookie signature to prevent easy tampering with cookie data + * + * @param string $value + * @return string + */ + protected static function hmac(string $value): string + { + return hash_hmac('sha1', $value, static::$key); + } - // compare the extracted hash with the hashed value - // don't accept value if the hash is invalid - if (hash_equals(static::hmac($value), $hash) !== true) { - return null; - } + /** + * Parses the hashed value from a cookie + * and tries to extract the value + * + * @param string $string + * @return mixed + */ + protected static function parse(string $string) + { + // if no hash-value separator is present, we can't parse the value + if (strpos($string, '+') === false) { + return null; + } - return $value; - } + // extract hash and value + $hash = Str::before($string, '+'); + $value = Str::after($string, '+'); - /** - * Remove a cookie - * - * - * - * cookie::remove('mycookie'); - * // mycookie is now gone - * - * - * - * @param string $key The name of the cookie - * @return bool true: the cookie has been removed, - * false: the cookie could not be removed - */ - public static function remove(string $key): bool - { - if (isset($_COOKIE[$key])) { - unset($_COOKIE[$key]); - return setcookie($key, '', 1, '/') && setcookie($key, false); - } + // if the hash or the value is missing at all return null + // $value can be an empty string, $hash can't be! + if (!is_string($hash) || $hash === '' || !is_string($value)) { + return null; + } - return false; - } + // compare the extracted hash with the hashed value + // don't accept value if the hash is invalid + if (hash_equals(static::hmac($value), $hash) !== true) { + return null; + } + + return $value; + } + + /** + * Remove a cookie + * + * + * + * cookie::remove('mycookie'); + * // mycookie is now gone + * + * + * + * @param string $key The name of the cookie + * @return bool true: the cookie has been removed, + * false: the cookie could not be removed + */ + public static function remove(string $key): bool + { + if (isset($_COOKIE[$key])) { + unset($_COOKIE[$key]); + return setcookie($key, '', 1, '/') && setcookie($key, false); + } + + return false; + } + + /** + * Tells the CMS responder that the response relies on a cookie and + * its value (even if the cookie isn't set in the current request); + * this ensures that the response is only cached for visitors who don't + * have this cookie set; + * https://github.com/getkirby/kirby/issues/4423#issuecomment-1166300526 + * + * @param string $key + * @return void + */ + protected static function trackUsage(string $key): void + { + // lazily request the instance for non-CMS use cases + $kirby = App::instance(null, true); + + if ($kirby) { + $kirby->response()->usesCookie($key); + } + } } diff --git a/kirby/src/Http/Environment.php b/kirby/src/Http/Environment.php new file mode 100644 index 0000000..68aedd4 --- /dev/null +++ b/kirby/src/Http/Environment.php @@ -0,0 +1,1128 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +class Environment +{ + /** + * Full base URL object + * + * @var \Kirby\Http\Uri + */ + protected $baseUri; + + /** + * Full base URL + * + * @var string + */ + protected $baseUrl; + + /** + * Whether the request is being served by the CLI + * + * @var bool + */ + protected $cli; + + /** + * Current host name + * + * @var string + */ + protected $host; + + /** + * Whether the HTTPS protocol is used + * + * @var bool + */ + protected $https; + + /** + * Sanitized `$_SERVER` data + * + * @var array + */ + protected $info; + + /** + * Current server's IP address + * + * @var string + */ + protected $ip; + + /** + * Whether the site is behind a reverse proxy; + * `null` if not known (fixed allowed URL setup) + * + * @var bool|null + */ + protected $isBehindProxy; + + /** + * URI path to the base + * + * @var string + */ + protected $path; + + /** + * Port number in the site URL + * + * @var int|null + */ + protected $port; + + /** + * Intermediary value of the port + * extracted from the host name + * + * @var int|null + */ + protected $portInHost; + + /** + * Uri object for the full request URI. + * It is a combination of the base URL and `REQUEST_URI` + * + * @var \Kirby\Http\Uri + */ + protected $requestUri; + + /** + * Full request URL + * + * @var string + */ + protected $requestUrl; + + /** + * Path to the php script within the + * document root without the + * filename of the script + * + * @var string + */ + protected $scriptPath; + + /** + * Class constructor + * + * @param array|null $options + * @param array|null $info Optional override for `$_SERVER` + */ + public function __construct(?array $options = null, ?array $info = null) + { + $this->detect($options, $info); + } + + /** + * Returns the server's IP address + * + * @see static::ip + * @return string|null + */ + public function address(): ?string + { + return $this->ip(); + } + + /** + * Returns the full base URL object + * + * @return \Kirby\Http\Uri + */ + public function baseUri() + { + return $this->baseUri; + } + + /** + * Returns the full base URL + * + * @return string + */ + public function baseUrl(): string + { + return $this->baseUrl; + } + + /** + * Checks if the request is being served by the CLI + * + * @return bool + */ + public function cli(): bool + { + return $this->cli; + } + + /** + * Sanitizes the server info and detects + * all relevant parts. This can be called + * again at a later point to overwrite all + * the stored information and re-detect the + * environment if necessary. + * + * @param array|null $options + * @param array|null $info Optional override for `$_SERVER` + * @return array + */ + public function detect(array $options = null, array $info = null): array + { + $info ??= $_SERVER; + $options = array_merge([ + 'cli' => null, + 'allowed' => null + ], $options ?? []); + + $this->info = static::sanitize($info); + $this->cli = $this->detectCli($options['cli']); + $this->ip = $this->detectIp(); + $this->host = null; + $this->https = false; + $this->isBehindProxy = null; + $this->scriptPath = $this->detectScriptPath($this->get('SCRIPT_NAME')); + $this->path = $this->detectPath($this->scriptPath); + $this->port = null; + + // keep Server flags compatible for now + // TODO: remove in 3.8.0 + // @codeCoverageIgnoreStart + if (is_int($options['allowed']) === true) { + Helpers::deprecated(' + Using `Server::` constants for the `allowed` option has been deprecated and support will be removed in 3.8.0. Use one of the following instead: a single fixed URL, an array of allowed URLs to match dynamically, `*` wildcard to match dynamically even from insecure headers, or `true` to match automtically from safe server variables. + '); + + $options['allowed'] = $this->detectAllowedFromFlag($options['allowed']); + } + // @codeCoverageIgnoreEnd + + // insecure auto-detection + if ($options['allowed'] === '*' || $options['allowed'] === ['*']) { + $this->detectAuto(true); + + // fixed environments + } elseif (empty($options['allowed']) === false) { + $this->detectAllowed($options['allowed']); + + // secure auto-detection + } else { + $this->detectAuto(); + } + + // build the URI based on the detected params + $this->detectBaseUri(); + + // build the request URI based on the detected URL + $this->detectRequestUri($this->get('REQUEST_URI')); + + // return the sanitized $_SERVER array + return $this->info; + } + + /** + * Sets the host name, port, path and protocol from the + * fixed list of allowed URLs + * + * @param array|string $allowed + * @return void + */ + protected function detectAllowed($allowed): void + { + $allowed = A::wrap($allowed); + + // with a single allowed URL, the entire + // environment will be based on that + if (count($allowed) === 1) { + $baseUrl = A::first($allowed); + + if (is_string($baseUrl) === false) { + throw new InvalidArgumentException('Invalid allow list setup for base URLs'); + } + + $uri = new Uri($baseUrl, ['slash' => false]); + + $this->host = $uri->host(); + $this->https = $uri->https(); + $this->port = $uri->port(); + $this->path = $uri->path()->toString(); + return; + } + + // run insecure auto detection to get + // host, port and https from the environment; + // security is achieved by checking against + // the fixed allowlist below + $this->detectAuto(true); + + // build the baseUrl based on the detected environment + // to compare it against what is allowed + $this->detectBaseUri(); + + foreach ($allowed as $url) { + // skip invalid URLs + if (is_string($url) === false) { + continue; + } + + $uri = new Uri($url, ['slash' => false]); + + if ($uri->toString() === $this->baseUrl) { + // the current environment is allowed, + // stop before the exception below is thrown + return; + } + } + + throw new InvalidArgumentException('The environment is not allowed'); + } + + /** + * The URL option receives a set of Server constant flags + * + * Server::HOST_FROM_SERVER + * Server::HOST_FROM_SERVER | Server::HOST_ALLOW_EMPTY + * Server::HOST_FROM_HEADER + * Server::HOST_FROM_HEADER | Server::HOST_ALLOW_EMPTY + * @todo Remove in 3.8.0 + * + * @param int $flags + * @return string|null + */ + protected function detectAllowedFromFlag(int $flags): ?string + { + // allow host detection from host headers + if ($flags & Server::HOST_FROM_HEADER) { + return '*'; + } + + // detect host only from server name + return null; + } + + /** + * Sets the host name, port and protocol without configuration + * + * @param bool $insecure Include the `Host`, `Forwarded` and `X-Forwarded-*` headers in the search + * @return void + */ + protected function detectAuto(bool $insecure = false): void + { + // proxy server setup + if ($insecure === true) { + $forwarded = $this->detectForwarded(); + + $host = $forwarded['host']; + $port = $forwarded['port']; + $https = $forwarded['https']; + + if ($host || $port || $https) { + $this->isBehindProxy = true; + + // if a port or scheme is defined but no host, assume + // that the host is the same as PHP's own hostname + // (which is often the case with reverse proxies) + $this->host = $host ?? $this->detectHost($insecure); + $this->port = $port; + $this->https = $https; + + return; + } + } + + // local server setup + $this->isBehindProxy = false; + + $this->host = $this->detectHost($insecure); + $this->https = $this->detectHttps(); + $this->port = $this->detectPort(); + } + + /** + * Builds the base URL based on the + * given environment params + * + * @return \Kirby\Http\Uri + */ + protected function detectBaseUri() + { + $this->baseUri = new Uri([ + 'host' => $this->host, + 'path' => $this->path, + 'port' => $this->port, + 'scheme' => $this->https ? 'https' : 'http', + ]); + + $this->baseUrl = $this->baseUri->toString(); + + return $this->baseUri; + } + + /** + * Detects if the request is served by the CLI + * + * @param bool|null $override Set to a boolean to override detection (for testing) + * @return bool + */ + protected function detectCli(?bool $override = null): bool + { + if (is_bool($override) === true) { + return $override; + } + + if (defined('STDIN') === true) { + return true; + } + + // @codeCoverageIgnoreStart + $term = getenv('TERM'); + + if (substr(PHP_SAPI, 0, 3) === 'cgi' && $term && $term !== 'unknown') { + return true; + } + + return false; + // @codeCoverageIgnoreEnd + } + + /** + * Detects the host, protocol, port and client IP + * from the `Forwarded` and `X-Forwarded-*` headers + * + * @return array + */ + protected function detectForwarded(): array + { + $data = [ + 'for' => null, + 'host' => null, + 'https' => false, + 'port' => null + ]; + + // prefer the standardized `Forwarded` header if defined + $forwarded = $this->get('HTTP_FORWARDED'); + if ($forwarded) { + // only use the first (outermost) proxy by using the first set of values + // before the first comma (but only a comma outside of quotes) + if (Str::contains($forwarded, ',') === true) { + $forwarded = preg_split('/"[^"]*"(*SKIP)(*F)|,/', $forwarded)[0]; + } + + // split into separate key=value;key=value fields by semicolon, + // but only split outside of quotes + $rawFields = preg_split('/"[^"]*"(*SKIP)(*F)|;/', $forwarded); + + // split key and value into an associative array + $fields = []; + foreach ($rawFields as $field) { + $key = Str::lower(Str::before($field, '=')); + $value = Str::after($field, '='); + + // trim the surrounding quotes + if (Str::substr($value, 0, 1) === '"') { + $value = Str::substr($value, 1, -1); + } + + $fields[$key] = $value; + } + + // assemble the normalized data + if (isset($fields['host']) === true) { + $parts = $this->detectPortInHost($fields['host']); + $data['host'] = $parts['host']; + $data['port'] = $parts['port']; + } + + if (isset($fields['proto']) === true) { + $data['https'] = $this->detectHttpsProtocol($fields['proto']); + } + + if ($data['port'] === null && $data['https'] === true) { + $data['port'] = 443; + } + + $data['for'] = $parts['for'] ?? null; + + return $data; + } + + // no success, try the `X-Forwarded-*` headers + $data['host'] = $this->detectForwardedHost(); + $data['https'] = $this->detectForwardedHttps(); + $data['port'] = $this->detectForwardedPort($data['https']); + $data['for'] = $this->get('HTTP_X_FORWARDED_FOR'); + + return $data; + } + + /** + * Detects the host name of the reverse proxy + * from the `X-Forwarded-Host` header + * + * @return string|null + */ + protected function detectForwardedHost(): ?string + { + $host = $this->get('HTTP_X_FORWARDED_HOST'); + $parts = $this->detectPortInHost($host); + + $this->portInHost = $parts['port']; + + return $parts['host']; + } + + /** + * Detects the protocol of the reverse proxy from the + * `X-Forwarded-SSL` or `X-Forwarded-Proto` header + * + * @return bool + */ + protected function detectForwardedHttps(): bool + { + if ($this->detectHttpsOn($this->get('HTTP_X_FORWARDED_SSL')) === true) { + return true; + } + + if ($this->detectHttpsProtocol($this->get('HTTP_X_FORWARDED_PROTO')) === true) { + return true; + } + + return false; + } + + /** + * Detects the port of the reverse proxy from the + * `X-Forwarded-Host` or `X-Forwarded-Port` header + * + * @param bool $https Whether HTTPS was detected + * @return int|null + */ + protected function detectForwardedPort(bool $https): ?int + { + // based on forwarded port + $port = $this->get('HTTP_X_FORWARDED_PORT'); + + if (is_int($port) === true) { + return $port; + } + + // based on forwarded host + if (is_int($this->portInHost) === true) { + return $this->portInHost; + } + + // based on the detected https state + if ($https === true) { + return 443; + } + + return null; + } + + /** + * Detects the host name from various headers + * + * @param bool $insecure Include the `Host` header in the search + * @return string|null + */ + protected function detectHost(bool $insecure = false): ?string + { + if ($insecure === true) { + $hosts[] = $this->get('HTTP_HOST'); + } + + $hosts[] = $this->get('SERVER_NAME'); + $hosts[] = $this->get('SERVER_ADDR'); + + // use the first header that is not empty + $hosts = array_filter($hosts); + $host = A::first($hosts); + + $parts = $this->detectPortInHost($host); + + $this->portInHost = $parts['port']; + + return $parts['host']; + } + + /** + * Detects the HTTPS status + * + * @return bool + */ + protected function detectHttps(): bool + { + if ($this->detectHttpsOn($this->get('HTTPS')) === true) { + return true; + } + + return false; + } + + /** + * Normalizes the HTTPS status into a boolean + * + * @param string|bool|null|int $value + * @return bool + */ + protected function detectHttpsOn($value): bool + { + // off can mean many things :) + $off = ['off', null, '', 0, '0', false, 'false', -1, '-1']; + + return in_array($value, $off, true) === false; + } + + /** + * Detects the HTTPS status from a `X-Forwarded-Proto` string + * + * @param string|null $protocol + * @return bool + */ + protected function detectHttpsProtocol(?string $protocol = null): bool + { + if ($protocol === null) { + return false; + } + + return in_array(strtolower($protocol), ['https', 'https, http']) === true; + } + + /** + * Detects the server's IP address + * + * @return string|null + */ + protected function detectIp(): ?string + { + return $this->get('SERVER_ADDR'); + } + + /** + * Detects the URI path unless in CLI mode + * + * @param string|null $path + * @return string + */ + protected function detectPath(?string $path = null): string + { + if ($this->cli === true) { + return ''; + } + + return $path ?? ''; + } + + /** + * Detects the port from various sources + * + * @return int|null + */ + protected function detectPort(): ?int + { + // based on server port + $port = $this->get('SERVER_PORT'); + + if (is_int($port) === true) { + return $port; + } + + // based on the detected host + if (is_int($this->portInHost) === true) { + return $this->portInHost; + } + + // based on the detected https state + if ($this->https === true) { + return 443; + } + + return null; + } + + /** + * Splits a hostname:port string into its components + * + * @param string|null $host + * @return array + */ + protected function detectPortInHost(?string $host = null): array + { + if (empty($host) === true) { + return [ + 'host' => null, + 'port' => null + ]; + } + + $parts = Str::split($host, ':'); + + return [ + 'host' => $parts[0] ?? null, + 'port' => static::sanitizePort($parts[1] ?? null), + ]; + } + + /** + * Splits any URI into path and query + * + * @param string|null $requestUri + * @return \Kirby\Http\Uri + */ + protected function detectRequestUri(?string $requestUri = null) + { + // make sure the URL parser works properly when there's a + // colon in the request URI but the URI is relative + if (Url::isAbsolute($requestUri) === false) { + $requestUri = 'https://getkirby.com' . $requestUri; + } + + $uri = new Uri($requestUri); + + // create the URI object as a combination of base uri parts + // and the parts from REQUEST_URI + $this->requestUri = $this->baseUri()->clone([ + 'fragment' => $uri->fragment(), + 'params' => $uri->params(), + 'path' => $uri->path(), + 'query' => $uri->query() + ]); + + // build the full request URL + $this->requestUrl = $this->requestUri->toString(); + + return $this->requestUri; + } + + /** + * Returns the sanitized script path unless in CLI mode + * + * @param string|null $scriptPath + * @return string + */ + protected function detectScriptPath(?string $scriptPath = null): string + { + if ($this->cli === true) { + return ''; + } + + return $this->sanitizeScriptPath($scriptPath); + } + + /** + * Gets a value from the server environment array + * + * + * $server->get('document_root'); + * // sample output: /var/www/kirby + * + * $server->get(); + * // returns the whole server array + * + * + * @param string|false|null $key The key to look for. Pass `false` or `null` + * to return the entire server array. + * @param mixed $default Optional default value, which should be + * returned if no element has been found + * @return mixed + */ + public function get($key = null, $default = null) + { + if (is_string($key) === false) { + return $this->info; + } + + if (isset($this->info[$key]) === false) { + $key = strtoupper($key); + } + + return $this->info[$key] ?? static::sanitize($key, $default); + } + + /** + * Gets a value from the global server environment array + * of the current app instance; falls back to `$_SERVER` if + * no app instance is running + * + * @param string|false|null $key The key to look for. Pass `false` or `null` + * to return the entire server array. + * @param mixed $default Optional default value, which should be + * returned if no element has been found + * @return mixed + */ + public static function getGlobally($key = null, $default = null) + { + // first try the global `Environment` object if the CMS is running + $app = App::instance(null, true); + if ($app) { + return $app->environment()->get($key, $default); + } + + if (is_string($key) === false) { + return static::sanitize($_SERVER); + } + + if (isset($_SERVER[$key]) === false) { + $key = strtoupper($key); + } + + return static::sanitize($key, $_SERVER[$key] ?? $default); + } + + /** + * Returns the current host name + * + * @return string|null + */ + public function host(): ?string + { + return $this->host; + } + + /** + * Returns whether the HTTPS protocol is used + * + * @return bool + */ + public function https(): bool + { + return $this->https; + } + + /** + * Returns the sanitized `$_SERVER` array + * + * @return array + */ + public function info(): array + { + return $this->info; + } + + /** + * Returns the server's IP address + * + * @return string|null + */ + public function ip(): ?string + { + return $this->ip; + } + + /** + * Returns if the server is behind a + * reverse proxy server + * + * @return bool|null + */ + public function isBehindProxy(): ?bool + { + return $this->isBehindProxy; + } + + /** + * Checks if this is a local installation; + * returns `false` if in doubt + * + * @return bool + */ + public function isLocal(): bool + { + // check host + $host = $this->host(); + + if ($host === 'localhost') { + return true; + } + + if (Str::endsWith($host, '.local') === true) { + return true; + } + + if (Str::endsWith($host, '.test') === true) { + return true; + } + + // collect all possible visitor ips + $ips = [ + $this->get('REMOTE_ADDR'), + $this->get('HTTP_X_FORWARDED_FOR'), + $this->get('HTTP_CLIENT_IP') + ]; + + if ($this->get('HTTP_FORWARDED')) { + $ips[] = $this->detectForwarded()['for']; + } + + // remove duplicates and empty ips + $ips = array_unique(array_filter($ips)); + + // no known ip? Better not assume it's local + if (empty($ips) === true) { + return false; + } + + // stop as soon as a non-local ip is found + foreach ($ips as $ip) { + if (in_array($ip, ['::1', '127.0.0.1']) === false) { + return false; + } + } + + return true; + } + + /** + * Loads and returns options from environment-specific + * PHP files (by host name and server IP address) + * + * @param string $root Root directory to load configs from + * @return array + */ + public function options(string $root): array + { + $configHost = []; + $configAddr = []; + + $host = $this->host(); + $addr = $this->ip(); + + // load the config for the host + if (empty($host) === false) { + $configHost = F::load($root . '/config.' . $host . '.php', []); + } + + // load the config for the server IP + if (empty($addr) === false) { + $configAddr = F::load($root . '/config.' . $addr . '.php', []); + } + + return array_replace_recursive($configHost, $configAddr); + } + + /** + * Returns the detected path + * + * @return string|null + */ + public function path(): ?string + { + return $this->path; + } + + /** + * Returns the correct port number + * + * @return int|null + */ + public function port(): ?int + { + return $this->port; + } + + /** + * Returns an URI object for the requested URL + * + * @return \Kirby\Http\Uri + */ + public function requestUri() + { + return $this->requestUri; + } + + /** + * Returns the current URL, including the request path + * and query + * + * @return string + */ + public function requestUrl(): string + { + return $this->requestUrl; + } + + /** + * Sanitizes some `$_SERVER` keys + * + * @param string|array $key + * @param mixed $value + * @return mixed + */ + public static function sanitize($key, $value = null) + { + if (is_array($key) === true) { + foreach ($key as $k => $v) { + $key[$k] = static::sanitize($k, $v); + } + + return $key; + } + + switch ($key) { + case 'SERVER_ADDR': + case 'SERVER_NAME': + case 'HTTP_HOST': + case 'HTTP_X_FORWARDED_HOST': + return static::sanitizeHost($value); + case 'SERVER_PORT': + case 'HTTP_X_FORWARDED_PORT': + return static::sanitizePort($value); + default: + return $value; + } + } + + /** + * Sanitizes the given host name + * + * @param string|null $host + * @return string|null + */ + protected static function sanitizeHost(?string $host = null): ?string + { + if (empty($host) === true) { + return null; + } + + $host = Str::lower($host); + $host = strip_tags($host); + $host = basename($host); + $host = preg_replace('![^\w.:-]+!iu', '', $host); + $host = htmlspecialchars($host, ENT_COMPAT); + $host = trim($host, '-'); + $host = trim($host, '.'); + $host = trim($host); + + if ($host === '') { + return null; + } + + return $host; + } + + /** + * Sanitizes the given port number + * + * @param string|int|null $port + * @return int|null + */ + protected static function sanitizePort($port = null): ?int + { + // already fine + if (is_int($port) === true) { + return $port; + } + + // no port given + if ($port === null || $port === false || $port === '') { + return null; + } + + // remove any character that is not an integer + $port = preg_replace('![^0-9]+!', '', (string)($port ?? '')); + + // no port + if ($port === '') { + return null; + } + + // convert to integer + return (int)$port; + } + + /** + * Sanitizes the given script path + * + * @param string|null $scriptPath + * @return string + */ + protected function sanitizeScriptPath(?string $scriptPath = null): string + { + $scriptPath ??= ''; + $scriptPath = trim($scriptPath); + + // skip all the sanitizing steps if the path is empty + if ($scriptPath === '') { + return $scriptPath; + } + + // replace Windows backslashes + $scriptPath = str_replace('\\', '/', $scriptPath); + // remove the script + $scriptPath = dirname($scriptPath); + // replace those fucking backslashes again + $scriptPath = str_replace('\\', '/', $scriptPath); + // remove the leading and trailing slashes + $scriptPath = trim($scriptPath, '/'); + + // top-level scripts don't have a path + // and dirname() will return '.' + if ($scriptPath === '.') { + return ''; + } + + return $scriptPath; + } + + /** + * Returns the path to the php script + * within the document root without the + * filename of the script. + * + * i.e. /subfolder/index.php -> subfolder + * + * This can be used to build the base baseUrl + * for subfolder installations + * + * @return string + */ + public function scriptPath(): string + { + return $this->scriptPath; + } + + /** + * Returns all environment data as array + * + * @return array + */ + public function toArray(): array + { + return [ + 'baseUrl' => $this->baseUrl, + 'host' => $this->host, + 'https' => $this->https, + 'info' => $this->info, + 'ip' => $this->ip, + 'isBehindProxy' => $this->isBehindProxy, + 'path' => $this->path, + 'port' => $this->port, + 'requestUrl' => $this->requestUrl, + 'scriptPath' => $this->scriptPath, + ]; + } +} diff --git a/kirby/src/Http/Header.php b/kirby/src/Http/Header.php index 2c5dc71..5f3b5ee 100644 --- a/kirby/src/Http/Header.php +++ b/kirby/src/Http/Header.php @@ -16,301 +16,301 @@ use Kirby\Filesystem\F; */ class Header { - // configuration - public static $codes = [ + // configuration + public static $codes = [ - // successful - '_200' => 'OK', - '_201' => 'Created', - '_202' => 'Accepted', + // successful + '_200' => 'OK', + '_201' => 'Created', + '_202' => 'Accepted', - // redirection - '_300' => 'Multiple Choices', - '_301' => 'Moved Permanently', - '_302' => 'Found', - '_303' => 'See Other', - '_304' => 'Not Modified', - '_307' => 'Temporary Redirect', - '_308' => 'Permanent Redirect', + // redirection + '_300' => 'Multiple Choices', + '_301' => 'Moved Permanently', + '_302' => 'Found', + '_303' => 'See Other', + '_304' => 'Not Modified', + '_307' => 'Temporary Redirect', + '_308' => 'Permanent Redirect', - // client error - '_400' => 'Bad Request', - '_401' => 'Unauthorized', - '_402' => 'Payment Required', - '_403' => 'Forbidden', - '_404' => 'Not Found', - '_405' => 'Method Not Allowed', - '_406' => 'Not Acceptable', - '_410' => 'Gone', - '_418' => 'I\'m a teapot', - '_451' => 'Unavailable For Legal Reasons', + // client error + '_400' => 'Bad Request', + '_401' => 'Unauthorized', + '_402' => 'Payment Required', + '_403' => 'Forbidden', + '_404' => 'Not Found', + '_405' => 'Method Not Allowed', + '_406' => 'Not Acceptable', + '_410' => 'Gone', + '_418' => 'I\'m a teapot', + '_451' => 'Unavailable For Legal Reasons', - // server error - '_500' => 'Internal Server Error', - '_501' => 'Not Implemented', - '_502' => 'Bad Gateway', - '_503' => 'Service Unavailable', - '_504' => 'Gateway Time-out' - ]; + // server error + '_500' => 'Internal Server Error', + '_501' => 'Not Implemented', + '_502' => 'Bad Gateway', + '_503' => 'Service Unavailable', + '_504' => 'Gateway Time-out' + ]; - /** - * Sends a content type header - * - * @param string $mime - * @param string $charset - * @param bool $send - * @return string|void - */ - public static function contentType(string $mime, string $charset = 'UTF-8', bool $send = true) - { - if ($found = F::extensionToMime($mime)) { - $mime = $found; - } + /** + * Sends a content type header + * + * @param string $mime + * @param string $charset + * @param bool $send + * @return string|void + */ + public static function contentType(string $mime, string $charset = 'UTF-8', bool $send = true) + { + if ($found = F::extensionToMime($mime)) { + $mime = $found; + } - $header = 'Content-type: ' . $mime; + $header = 'Content-type: ' . $mime; - if (empty($charset) === false) { - $header .= '; charset=' . $charset; - } + if (empty($charset) === false) { + $header .= '; charset=' . $charset; + } - if ($send === false) { - return $header; - } + if ($send === false) { + return $header; + } - header($header); - } + header($header); + } - /** - * Creates headers by key and value - * - * @param string|array $key - * @param string|null $value - * @return string - */ - public static function create($key, string $value = null): string - { - if (is_array($key) === true) { - $headers = []; + /** + * Creates headers by key and value + * + * @param string|array $key + * @param string|null $value + * @return string + */ + public static function create($key, string $value = null): string + { + if (is_array($key) === true) { + $headers = []; - foreach ($key as $k => $v) { - $headers[] = static::create($k, $v); - } + foreach ($key as $k => $v) { + $headers[] = static::create($k, $v); + } - return implode("\r\n", $headers); - } + return implode("\r\n", $headers); + } - // prevent header injection by stripping any newline characters from single headers - return str_replace(["\r", "\n"], '', $key . ': ' . $value); - } + // prevent header injection by stripping any newline characters from single headers + return str_replace(["\r", "\n"], '', $key . ': ' . $value); + } - /** - * Shortcut for static::contentType() - * - * @param string $mime - * @param string $charset - * @param bool $send - * @return string|void - */ - public static function type(string $mime, string $charset = 'UTF-8', bool $send = true) - { - return static::contentType($mime, $charset, $send); - } + /** + * Shortcut for static::contentType() + * + * @param string $mime + * @param string $charset + * @param bool $send + * @return string|void + */ + public static function type(string $mime, string $charset = 'UTF-8', bool $send = true) + { + return static::contentType($mime, $charset, $send); + } - /** - * Sends a status header - * - * Checks $code against a list of known status codes. To bypass this check - * and send a custom status code and message, use a $code string formatted - * as 3 digits followed by a space and a message, e.g. '999 Custom Status'. - * - * @param int|string $code The HTTP status code - * @param bool $send If set to false the header will be returned instead - * @return string|void - */ - public static function status($code = null, bool $send = true) - { - $codes = static::$codes; - $protocol = $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.1'; + /** + * Sends a status header + * + * Checks $code against a list of known status codes. To bypass this check + * and send a custom status code and message, use a $code string formatted + * as 3 digits followed by a space and a message, e.g. '999 Custom Status'. + * + * @param int|string $code The HTTP status code + * @param bool $send If set to false the header will be returned instead + * @return string|void + */ + public static function status($code = null, bool $send = true) + { + $codes = static::$codes; + $protocol = Environment::getGlobally('SERVER_PROTOCOL', 'HTTP/1.1'); - // allow full control over code and message - if (is_string($code) === true && preg_match('/^\d{3} \w.+$/', $code) === 1) { - $message = substr(rtrim($code), 4); - $code = substr($code, 0, 3); - } else { - $code = array_key_exists('_' . $code, $codes) === false ? 500 : $code; - $message = $codes['_' . $code] ?? 'Something went wrong'; - } + // allow full control over code and message + if (is_string($code) === true && preg_match('/^\d{3} \w.+$/', $code) === 1) { + $message = substr(rtrim($code), 4); + $code = substr($code, 0, 3); + } else { + $code = array_key_exists('_' . $code, $codes) === false ? 500 : $code; + $message = $codes['_' . $code] ?? 'Something went wrong'; + } - $header = $protocol . ' ' . $code . ' ' . $message; + $header = $protocol . ' ' . $code . ' ' . $message; - if ($send === false) { - return $header; - } + if ($send === false) { + return $header; + } - // try to send the header - header($header); - } + // try to send the header + header($header); + } - /** - * Sends a 200 header - * - * @param bool $send - * @return string|void - */ - public static function success(bool $send = true) - { - return static::status(200, $send); - } + /** + * Sends a 200 header + * + * @param bool $send + * @return string|void + */ + public static function success(bool $send = true) + { + return static::status(200, $send); + } - /** - * Sends a 201 header - * - * @param bool $send - * @return string|void - */ - public static function created(bool $send = true) - { - return static::status(201, $send); - } + /** + * Sends a 201 header + * + * @param bool $send + * @return string|void + */ + public static function created(bool $send = true) + { + return static::status(201, $send); + } - /** - * Sends a 202 header - * - * @param bool $send - * @return string|void - */ - public static function accepted(bool $send = true) - { - return static::status(202, $send); - } + /** + * Sends a 202 header + * + * @param bool $send + * @return string|void + */ + public static function accepted(bool $send = true) + { + return static::status(202, $send); + } - /** - * Sends a 400 header - * - * @param bool $send - * @return string|void - */ - public static function error(bool $send = true) - { - return static::status(400, $send); - } + /** + * Sends a 400 header + * + * @param bool $send + * @return string|void + */ + public static function error(bool $send = true) + { + return static::status(400, $send); + } - /** - * Sends a 403 header - * - * @param bool $send - * @return string|void - */ - public static function forbidden(bool $send = true) - { - return static::status(403, $send); - } + /** + * Sends a 403 header + * + * @param bool $send + * @return string|void + */ + public static function forbidden(bool $send = true) + { + return static::status(403, $send); + } - /** - * Sends a 404 header - * - * @param bool $send - * @return string|void - */ - public static function notfound(bool $send = true) - { - return static::status(404, $send); - } + /** + * Sends a 404 header + * + * @param bool $send + * @return string|void + */ + public static function notfound(bool $send = true) + { + return static::status(404, $send); + } - /** - * Sends a 404 header - * - * @param bool $send - * @return string|void - */ - public static function missing(bool $send = true) - { - return static::status(404, $send); - } + /** + * Sends a 404 header + * + * @param bool $send + * @return string|void + */ + public static function missing(bool $send = true) + { + return static::status(404, $send); + } - /** - * Sends a 410 header - * - * @param bool $send - * @return string|void - */ - public static function gone(bool $send = true) - { - return static::status(410, $send); - } + /** + * Sends a 410 header + * + * @param bool $send + * @return string|void + */ + public static function gone(bool $send = true) + { + return static::status(410, $send); + } - /** - * Sends a 500 header - * - * @param bool $send - * @return string|void - */ - public static function panic(bool $send = true) - { - return static::status(500, $send); - } + /** + * Sends a 500 header + * + * @param bool $send + * @return string|void + */ + public static function panic(bool $send = true) + { + return static::status(500, $send); + } - /** - * Sends a 503 header - * - * @param bool $send - * @return string|void - */ - public static function unavailable(bool $send = true) - { - return static::status(503, $send); - } + /** + * Sends a 503 header + * + * @param bool $send + * @return string|void + */ + public static function unavailable(bool $send = true) + { + return static::status(503, $send); + } - /** - * Sends a redirect header - * - * @param string $url - * @param int $code - * @param bool $send - * @return string|void - */ - public static function redirect(string $url, int $code = 302, bool $send = true) - { - $status = static::status($code, false); - $location = 'Location:' . Url::unIdn($url); + /** + * Sends a redirect header + * + * @param string $url + * @param int $code + * @param bool $send + * @return string|void + */ + public static function redirect(string $url, int $code = 302, bool $send = true) + { + $status = static::status($code, false); + $location = 'Location:' . Url::unIdn($url); - if ($send !== true) { - return $status . "\r\n" . $location; - } + if ($send !== true) { + return $status . "\r\n" . $location; + } - header($status); - header($location); - exit(); - } + header($status); + header($location); + exit(); + } - /** - * Sends download headers for anything that is downloadable - * - * @param array $params Check out the defaults array for available parameters - */ - public static function download(array $params = []) - { - $defaults = [ - 'name' => 'download', - 'size' => false, - 'mime' => 'application/force-download', - 'modified' => time() - ]; + /** + * Sends download headers for anything that is downloadable + * + * @param array $params Check out the defaults array for available parameters + */ + public static function download(array $params = []) + { + $defaults = [ + 'name' => 'download', + 'size' => false, + 'mime' => 'application/force-download', + 'modified' => time() + ]; - $options = array_merge($defaults, $params); + $options = array_merge($defaults, $params); - header('Pragma: public'); - header('Cache-Control: no-cache, no-store, must-revalidate'); - header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT'); - header('Content-Disposition: attachment; filename="' . $options['name'] . '"'); - header('Content-Transfer-Encoding: binary'); + header('Pragma: public'); + header('Cache-Control: no-cache, no-store, must-revalidate'); + header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $options['modified']) . ' GMT'); + header('Content-Disposition: attachment; filename="' . $options['name'] . '"'); + header('Content-Transfer-Encoding: binary'); - static::contentType($options['mime']); + static::contentType($options['mime']); - if ($options['size']) { - header('Content-Length: ' . $options['size']); - } + if ($options['size']) { + header('Content-Length: ' . $options['size']); + } - header('Connection: close'); - } + header('Connection: close'); + } } diff --git a/kirby/src/Http/Idn.php b/kirby/src/Http/Idn.php index b8e1ff9..a9a538f 100644 --- a/kirby/src/Http/Idn.php +++ b/kirby/src/Http/Idn.php @@ -15,61 +15,61 @@ use Kirby\Toolkit\Str; */ class Idn { - /** - * Convert domain name from IDNA ASCII to Unicode - * - * @param string $domain - * @return string|false - */ - public static function decode(string $domain) - { - return idn_to_utf8($domain); - } + /** + * Convert domain name from IDNA ASCII to Unicode + * + * @param string $domain + * @return string|false + */ + public static function decode(string $domain) + { + return idn_to_utf8($domain); + } - /** - * Convert domain name to IDNA ASCII form - * - * @param string $domain - * @return string|false - */ - public static function encode(string $domain) - { - return idn_to_ascii($domain); - } + /** + * Convert domain name to IDNA ASCII form + * + * @param string $domain + * @return string|false + */ + public static function encode(string $domain) + { + return idn_to_ascii($domain); + } - /** - * Decodes a email address to the Unicode format - * - * @param string $email - * @return string - */ - public static function decodeEmail(string $email): string - { - if (Str::contains($email, 'xn--') === true) { - $parts = Str::split($email, '@'); - $address = $parts[0]; - $domain = Idn::decode($parts[1] ?? ''); - $email = $address . '@' . $domain; - } + /** + * Decodes a email address to the Unicode format + * + * @param string $email + * @return string + */ + public static function decodeEmail(string $email): string + { + if (Str::contains($email, 'xn--') === true) { + $parts = Str::split($email, '@'); + $address = $parts[0]; + $domain = Idn::decode($parts[1] ?? ''); + $email = $address . '@' . $domain; + } - return $email; - } + return $email; + } - /** - * Encodes a email address to the Punycode format - * - * @param string $email - * @return string - */ - public static function encodeEmail(string $email): string - { - if (mb_detect_encoding($email, 'ASCII', true) === false) { - $parts = Str::split($email, '@'); - $address = $parts[0]; - $domain = Idn::encode($parts[1] ?? ''); - $email = $address . '@' . $domain; - } + /** + * Encodes a email address to the Punycode format + * + * @param string $email + * @return string + */ + public static function encodeEmail(string $email): string + { + if (mb_detect_encoding($email, 'ASCII', true) === false) { + $parts = Str::split($email, '@'); + $address = $parts[0]; + $domain = Idn::encode($parts[1] ?? ''); + $email = $address . '@' . $domain; + } - return $email; - } + return $email; + } } diff --git a/kirby/src/Http/Params.php b/kirby/src/Http/Params.php index 5e0273d..f9c7e1a 100644 --- a/kirby/src/Http/Params.php +++ b/kirby/src/Http/Params.php @@ -17,142 +17,142 @@ use Kirby\Toolkit\Str; */ class Params extends Query { - /** - * @var null|string - */ - public static $separator; + /** + * @var null|string + */ + public static $separator; - /** - * Creates a new params object - * - * @param array|string $params - */ - public function __construct($params) - { - if (is_string($params) === true) { - $params = static::extract($params)['params']; - } + /** + * Creates a new params object + * + * @param array|string $params + */ + public function __construct($params) + { + if (is_string($params) === true) { + $params = static::extract($params)['params']; + } - parent::__construct($params ?? []); - } + parent::__construct($params ?? []); + } - /** - * Extract the params from a string or array - * - * @param string|array|null $path - * @return array - */ - public static function extract($path = null): array - { - if (empty($path) === true) { - return [ - 'path' => null, - 'params' => null, - 'slash' => false - ]; - } + /** + * Extract the params from a string or array + * + * @param string|array|null $path + * @return array + */ + public static function extract($path = null): array + { + if (empty($path) === true) { + return [ + 'path' => null, + 'params' => null, + 'slash' => false + ]; + } - $slash = false; + $slash = false; - if (is_string($path) === true) { - $slash = substr($path, -1, 1) === '/'; - $path = Str::split($path, '/'); - } + if (is_string($path) === true) { + $slash = substr($path, -1, 1) === '/'; + $path = Str::split($path, '/'); + } - if (is_array($path) === true) { - $params = []; - $separator = static::separator(); + if (is_array($path) === true) { + $params = []; + $separator = static::separator(); - foreach ($path as $index => $p) { - if (strpos($p, $separator) === false) { - continue; - } + foreach ($path as $index => $p) { + if (strpos($p, $separator) === false) { + continue; + } - $paramParts = Str::split($p, $separator); - $paramKey = $paramParts[0] ?? null; - $paramValue = $paramParts[1] ?? null; + $paramParts = Str::split($p, $separator); + $paramKey = $paramParts[0] ?? null; + $paramValue = $paramParts[1] ?? null; - if ($paramKey !== null) { - $params[$paramKey] = $paramValue; - } + if ($paramKey !== null) { + $params[rawurldecode($paramKey)] = $paramValue ? rawurldecode($paramValue) : null; + } - unset($path[$index]); - } + unset($path[$index]); + } - return [ - 'path' => $path, - 'params' => $params, - 'slash' => $slash - ]; - } + return [ + 'path' => $path, + 'params' => $params, + 'slash' => $slash + ]; + } - return [ - 'path' => null, - 'params' => null, - 'slash' => false - ]; - } + return [ + 'path' => null, + 'params' => null, + 'slash' => false + ]; + } - /** - * Returns the param separator according - * to the operating system. - * - * Unix = ':' - * Windows = ';' - * - * @return string - */ - public static function separator(): string - { - if (static::$separator !== null) { - return static::$separator; - } + /** + * Returns the param separator according + * to the operating system. + * + * Unix = ':' + * Windows = ';' + * + * @return string + */ + public static function separator(): string + { + if (static::$separator !== null) { + return static::$separator; + } - if (DIRECTORY_SEPARATOR === '/') { - return static::$separator = ':'; - } else { - return static::$separator = ';'; - } - } + if (DIRECTORY_SEPARATOR === '/') { + return static::$separator = ':'; + } else { + return static::$separator = ';'; + } + } - /** - * Converts the params object to a params string - * which can then be used in the URL builder again - * - * @param bool $leadingSlash - * @param bool $trailingSlash - * @return string|null - * - * @todo The argument $leadingSlash is incompatible with - * Query::toString($questionMark = false); the Query class - * should be extracted into a common parent class for both - * Query and Params - * @psalm-suppress ParamNameMismatch - */ - public function toString($leadingSlash = false, $trailingSlash = false): string - { - if ($this->isEmpty() === true) { - return ''; - } + /** + * Converts the params object to a params string + * which can then be used in the URL builder again + * + * @param bool $leadingSlash + * @param bool $trailingSlash + * @return string|null + * + * @todo The argument $leadingSlash is incompatible with + * Query::toString($questionMark = false); the Query class + * should be extracted into a common parent class for both + * Query and Params + * @psalm-suppress ParamNameMismatch + */ + public function toString($leadingSlash = false, $trailingSlash = false): string + { + if ($this->isEmpty() === true) { + return ''; + } - $params = []; - $separator = static::separator(); + $params = []; + $separator = static::separator(); - foreach ($this as $key => $value) { - if ($value !== null && $value !== '') { - $params[] = $key . $separator . $value; - } - } + foreach ($this as $key => $value) { + if ($value !== null && $value !== '') { + $params[] = rawurlencode($key) . $separator . rawurlencode($value); + } + } - if (empty($params) === true) { - return ''; - } + if (empty($params) === true) { + return ''; + } - $params = implode('/', $params); + $params = implode('/', $params); - $leadingSlash = $leadingSlash === true ? '/' : null; - $trailingSlash = $trailingSlash === true ? '/' : null; + $leadingSlash = $leadingSlash === true ? '/' : null; + $trailingSlash = $trailingSlash === true ? '/' : null; - return $leadingSlash . $params . $trailingSlash; - } + return $leadingSlash . $params . $trailingSlash; + } } diff --git a/kirby/src/Http/Path.php b/kirby/src/Http/Path.php index 321591b..ca4a77d 100644 --- a/kirby/src/Http/Path.php +++ b/kirby/src/Http/Path.php @@ -17,31 +17,31 @@ use Kirby\Toolkit\Str; */ class Path extends Collection { - public function __construct($items) - { - if (is_string($items) === true) { - $items = Str::split($items, '/'); - } + public function __construct($items) + { + if (is_string($items) === true) { + $items = Str::split($items, '/'); + } - parent::__construct($items ?? []); - } + parent::__construct($items ?? []); + } - public function __toString(): string - { - return $this->toString(); - } + public function __toString(): string + { + return $this->toString(); + } - public function toString(bool $leadingSlash = false, bool $trailingSlash = false): string - { - if (empty($this->data) === true) { - return ''; - } + public function toString(bool $leadingSlash = false, bool $trailingSlash = false): string + { + if (empty($this->data) === true) { + return ''; + } - $path = implode('/', $this->data); + $path = implode('/', $this->data); - $leadingSlash = $leadingSlash === true ? '/' : null; - $trailingSlash = $trailingSlash === true ? '/' : null; + $leadingSlash = $leadingSlash === true ? '/' : null; + $trailingSlash = $trailingSlash === true ? '/' : null; - return $leadingSlash . $path . $trailingSlash; - } + return $leadingSlash . $path . $trailingSlash; + } } diff --git a/kirby/src/Http/Query.php b/kirby/src/Http/Query.php index fe92b4b..89e7f50 100644 --- a/kirby/src/Http/Query.php +++ b/kirby/src/Http/Query.php @@ -17,42 +17,42 @@ use Kirby\Toolkit\Obj; */ class Query extends Obj { - public function __construct($query) - { - if (is_string($query) === true) { - parse_str(ltrim($query, '?'), $query); - } + public function __construct($query) + { + if (is_string($query) === true) { + parse_str(ltrim($query, '?'), $query); + } - parent::__construct($query ?? []); - } + parent::__construct($query ?? []); + } - public function isEmpty(): bool - { - return empty((array)$this) === true; - } + public function isEmpty(): bool + { + return empty((array)$this) === true; + } - public function isNotEmpty(): bool - { - return empty((array)$this) === false; - } + public function isNotEmpty(): bool + { + return empty((array)$this) === false; + } - public function __toString(): string - { - return $this->toString(); - } + public function __toString(): string + { + return $this->toString(); + } - public function toString($questionMark = false): string - { - $query = http_build_query($this, '', '&', PHP_QUERY_RFC3986); + public function toString($questionMark = false): string + { + $query = http_build_query($this, '', '&', PHP_QUERY_RFC3986); - if (empty($query) === true) { - return ''; - } + if (empty($query) === true) { + return ''; + } - if ($questionMark === true) { - $query = '?' . $query; - } + if ($questionMark === true) { + $query = '?' . $query; + } - return $query; - } + return $query; + } } diff --git a/kirby/src/Http/Remote.php b/kirby/src/Http/Remote.php index 612bd04..8325e33 100644 --- a/kirby/src/Http/Remote.php +++ b/kirby/src/Http/Remote.php @@ -20,392 +20,392 @@ use Kirby\Toolkit\Str; */ class Remote { - public const CA_INTERNAL = 1; - public const CA_SYSTEM = 2; + public const CA_INTERNAL = 1; + public const CA_SYSTEM = 2; - /** - * @var array - */ - public static $defaults = [ - 'agent' => null, - 'basicAuth' => null, - 'body' => true, - 'ca' => self::CA_INTERNAL, - 'data' => [], - 'encoding' => 'utf-8', - 'file' => null, - 'headers' => [], - 'method' => 'GET', - 'progress' => null, - 'test' => false, - 'timeout' => 10, - ]; + /** + * @var array + */ + public static $defaults = [ + 'agent' => null, + 'basicAuth' => null, + 'body' => true, + 'ca' => self::CA_INTERNAL, + 'data' => [], + 'encoding' => 'utf-8', + 'file' => null, + 'headers' => [], + 'method' => 'GET', + 'progress' => null, + 'test' => false, + 'timeout' => 10, + ]; - /** - * @var string - */ - public $content; + /** + * @var string + */ + public $content; - /** - * @var resource - */ - public $curl; + /** + * @var resource + */ + public $curl; - /** - * @var array - */ - public $curlopt = []; + /** + * @var array + */ + public $curlopt = []; - /** - * @var int - */ - public $errorCode; + /** + * @var int + */ + public $errorCode; - /** - * @var string - */ - public $errorMessage; + /** + * @var string + */ + public $errorMessage; - /** - * @var array - */ - public $headers = []; + /** + * @var array + */ + public $headers = []; - /** - * @var array - */ - public $info = []; + /** + * @var array + */ + public $info = []; - /** - * @var array - */ - public $options = []; + /** + * @var array + */ + public $options = []; - /** - * Magic getter for request info data - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public function __call(string $method, array $arguments = []) - { - $method = str_replace('-', '_', Str::kebab($method)); - return $this->info[$method] ?? null; - } + /** + * Magic getter for request info data + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public function __call(string $method, array $arguments = []) + { + $method = str_replace('-', '_', Str::kebab($method)); + return $this->info[$method] ?? null; + } - /** - * Constructor - * - * @param string $url - * @param array $options - */ - public function __construct(string $url, array $options = []) - { - $defaults = static::$defaults; + /** + * Constructor + * + * @param string $url + * @param array $options + */ + public function __construct(string $url, array $options = []) + { + $defaults = static::$defaults; - // use the system CA store by default if - // one has been configured in php.ini - $cainfo = ini_get('curl.cainfo'); - if (empty($cainfo) === false && is_file($cainfo) === true) { - $defaults['ca'] = self::CA_SYSTEM; - } + // use the system CA store by default if + // one has been configured in php.ini + $cainfo = ini_get('curl.cainfo'); + if (empty($cainfo) === false && is_file($cainfo) === true) { + $defaults['ca'] = self::CA_SYSTEM; + } - // update the defaults with App config if set; - // request the App instance lazily - $app = App::instance(null, true); - if ($app !== null) { - $defaults = array_merge($defaults, $app->option('remote', [])); - } + // update the defaults with App config if set; + // request the App instance lazily + $app = App::instance(null, true); + if ($app !== null) { + $defaults = array_merge($defaults, $app->option('remote', [])); + } - // set all options - $this->options = array_merge($defaults, $options); + // set all options + $this->options = array_merge($defaults, $options); - // add the url - $this->options['url'] = $url; + // add the url + $this->options['url'] = $url; - // send the request - $this->fetch(); - } + // send the request + $this->fetch(); + } - public static function __callStatic(string $method, array $arguments = []) - { - return new static($arguments[0], array_merge(['method' => strtoupper($method)], $arguments[1] ?? [])); - } + public static function __callStatic(string $method, array $arguments = []) + { + return new static($arguments[0], array_merge(['method' => strtoupper($method)], $arguments[1] ?? [])); + } - /** - * Returns the http status code - * - * @return int|null - */ - public function code(): ?int - { - return $this->info['http_code'] ?? null; - } + /** + * Returns the http status code + * + * @return int|null + */ + public function code(): ?int + { + return $this->info['http_code'] ?? null; + } - /** - * Returns the response content - * - * @return mixed - */ - public function content() - { - return $this->content; - } + /** + * Returns the response content + * + * @return mixed + */ + public function content() + { + return $this->content; + } - /** - * Sets up all curl options and sends the request - * - * @return $this - */ - public function fetch() - { - // curl options - $this->curlopt = [ - CURLOPT_URL => $this->options['url'], - CURLOPT_ENCODING => $this->options['encoding'], - CURLOPT_CONNECTTIMEOUT => $this->options['timeout'], - CURLOPT_TIMEOUT => $this->options['timeout'], - CURLOPT_AUTOREFERER => true, - CURLOPT_RETURNTRANSFER => $this->options['body'], - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 10, - CURLOPT_HEADER => false, - CURLOPT_HEADERFUNCTION => function ($curl, $header) { - $parts = Str::split($header, ':'); + /** + * Sets up all curl options and sends the request + * + * @return $this + */ + public function fetch() + { + // curl options + $this->curlopt = [ + CURLOPT_URL => $this->options['url'], + CURLOPT_ENCODING => $this->options['encoding'], + CURLOPT_CONNECTTIMEOUT => $this->options['timeout'], + CURLOPT_TIMEOUT => $this->options['timeout'], + CURLOPT_AUTOREFERER => true, + CURLOPT_RETURNTRANSFER => $this->options['body'], + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_MAXREDIRS => 10, + CURLOPT_HEADER => false, + CURLOPT_HEADERFUNCTION => function ($curl, $header) { + $parts = Str::split($header, ':'); - if (empty($parts[0]) === false && empty($parts[1]) === false) { - $key = array_shift($parts); - $this->headers[$key] = implode(':', $parts); - } + if (empty($parts[0]) === false && empty($parts[1]) === false) { + $key = array_shift($parts); + $this->headers[$key] = implode(':', $parts); + } - return strlen($header); - } - ]; + return strlen($header); + } + ]; - // determine the TLS CA to use - if ($this->options['ca'] === self::CA_INTERNAL) { - $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; - $this->curlopt[CURLOPT_CAINFO] = dirname(__DIR__, 2) . '/cacert.pem'; - } elseif ($this->options['ca'] === self::CA_SYSTEM) { - $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; - } elseif ($this->options['ca'] === false) { - $this->curlopt[CURLOPT_SSL_VERIFYPEER] = false; - } elseif ( - is_string($this->options['ca']) === true && - is_file($this->options['ca']) === true - ) { - $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; - $this->curlopt[CURLOPT_CAINFO] = $this->options['ca']; - } elseif ( - is_string($this->options['ca']) === true && - is_dir($this->options['ca']) === true - ) { - $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; - $this->curlopt[CURLOPT_CAPATH] = $this->options['ca']; - } else { - throw new InvalidArgumentException('Invalid "ca" option for the Remote class'); - } + // determine the TLS CA to use + if ($this->options['ca'] === self::CA_INTERNAL) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + $this->curlopt[CURLOPT_CAINFO] = dirname(__DIR__, 2) . '/cacert.pem'; + } elseif ($this->options['ca'] === self::CA_SYSTEM) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + } elseif ($this->options['ca'] === false) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = false; + } elseif ( + is_string($this->options['ca']) === true && + is_file($this->options['ca']) === true + ) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + $this->curlopt[CURLOPT_CAINFO] = $this->options['ca']; + } elseif ( + is_string($this->options['ca']) === true && + is_dir($this->options['ca']) === true + ) { + $this->curlopt[CURLOPT_SSL_VERIFYPEER] = true; + $this->curlopt[CURLOPT_CAPATH] = $this->options['ca']; + } else { + throw new InvalidArgumentException('Invalid "ca" option for the Remote class'); + } - // add the progress - if (is_callable($this->options['progress']) === true) { - $this->curlopt[CURLOPT_NOPROGRESS] = false; - $this->curlopt[CURLOPT_PROGRESSFUNCTION] = $this->options['progress']; - } + // add the progress + if (is_callable($this->options['progress']) === true) { + $this->curlopt[CURLOPT_NOPROGRESS] = false; + $this->curlopt[CURLOPT_PROGRESSFUNCTION] = $this->options['progress']; + } - // add all headers - if (empty($this->options['headers']) === false) { - // convert associative arrays to strings - $headers = []; - foreach ($this->options['headers'] as $key => $value) { - if (is_string($key) === true) { - $headers[] = $key . ': ' . $value; - } else { - $headers[] = $value; - } - } + // add all headers + if (empty($this->options['headers']) === false) { + // convert associative arrays to strings + $headers = []; + foreach ($this->options['headers'] as $key => $value) { + if (is_string($key) === true) { + $headers[] = $key . ': ' . $value; + } else { + $headers[] = $value; + } + } - $this->curlopt[CURLOPT_HTTPHEADER] = $headers; - } + $this->curlopt[CURLOPT_HTTPHEADER] = $headers; + } - // add HTTP Basic authentication - if (empty($this->options['basicAuth']) === false) { - $this->curlopt[CURLOPT_USERPWD] = $this->options['basicAuth']; - } + // add HTTP Basic authentication + if (empty($this->options['basicAuth']) === false) { + $this->curlopt[CURLOPT_USERPWD] = $this->options['basicAuth']; + } - // add the user agent - if (empty($this->options['agent']) === false) { - $this->curlopt[CURLOPT_USERAGENT] = $this->options['agent']; - } + // add the user agent + if (empty($this->options['agent']) === false) { + $this->curlopt[CURLOPT_USERAGENT] = $this->options['agent']; + } - // do some request specific stuff - switch (strtoupper($this->options['method'])) { - case 'POST': - $this->curlopt[CURLOPT_POST] = true; - $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'POST'; - $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); - break; - case 'PUT': - $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PUT'; - $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + // do some request specific stuff + switch (strtoupper($this->options['method'])) { + case 'POST': + $this->curlopt[CURLOPT_POST] = true; + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'POST'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'PUT': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PUT'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); - // put a file - if ($this->options['file']) { - $this->curlopt[CURLOPT_INFILE] = fopen($this->options['file'], 'r'); - $this->curlopt[CURLOPT_INFILESIZE] = F::size($this->options['file']); - } - break; - case 'PATCH': - $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PATCH'; - $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); - break; - case 'DELETE': - $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'DELETE'; - $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); - break; - case 'HEAD': - $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'HEAD'; - $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); - $this->curlopt[CURLOPT_NOBODY] = true; - break; - } + // put a file + if ($this->options['file']) { + $this->curlopt[CURLOPT_INFILE] = fopen($this->options['file'], 'r'); + $this->curlopt[CURLOPT_INFILESIZE] = F::size($this->options['file']); + } + break; + case 'PATCH': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'PATCH'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'DELETE': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + break; + case 'HEAD': + $this->curlopt[CURLOPT_CUSTOMREQUEST] = 'HEAD'; + $this->curlopt[CURLOPT_POSTFIELDS] = $this->postfields($this->options['data']); + $this->curlopt[CURLOPT_NOBODY] = true; + break; + } - if ($this->options['test'] === true) { - return $this; - } + if ($this->options['test'] === true) { + return $this; + } - // start a curl request - $this->curl = curl_init(); + // start a curl request + $this->curl = curl_init(); - curl_setopt_array($this->curl, $this->curlopt); + curl_setopt_array($this->curl, $this->curlopt); - $this->content = curl_exec($this->curl); - $this->info = curl_getinfo($this->curl); - $this->errorCode = curl_errno($this->curl); - $this->errorMessage = curl_error($this->curl); + $this->content = curl_exec($this->curl); + $this->info = curl_getinfo($this->curl); + $this->errorCode = curl_errno($this->curl); + $this->errorMessage = curl_error($this->curl); - if ($this->errorCode) { - throw new Exception($this->errorMessage, $this->errorCode); - } + if ($this->errorCode) { + throw new Exception($this->errorMessage, $this->errorCode); + } - curl_close($this->curl); + curl_close($this->curl); - return $this; - } + return $this; + } - /** - * Static method to send a GET request - * - * @param string $url - * @param array $params - * @return static - */ - public static function get(string $url, array $params = []) - { - $defaults = [ - 'method' => 'GET', - 'data' => [], - ]; + /** + * Static method to send a GET request + * + * @param string $url + * @param array $params + * @return static + */ + public static function get(string $url, array $params = []) + { + $defaults = [ + 'method' => 'GET', + 'data' => [], + ]; - $options = array_merge($defaults, $params); - $query = http_build_query($options['data']); + $options = array_merge($defaults, $params); + $query = http_build_query($options['data']); - if (empty($query) === false) { - $url = Url::hasQuery($url) === true ? $url . '&' . $query : $url . '?' . $query; - } + if (empty($query) === false) { + $url = Url::hasQuery($url) === true ? $url . '&' . $query : $url . '?' . $query; + } - // remove the data array from the options - unset($options['data']); + // remove the data array from the options + unset($options['data']); - return new static($url, $options); - } + return new static($url, $options); + } - /** - * Returns all received headers - * - * @return array - */ - public function headers(): array - { - return $this->headers; - } + /** + * Returns all received headers + * + * @return array + */ + public function headers(): array + { + return $this->headers; + } - /** - * Returns the request info - * - * @return array - */ - public function info(): array - { - return $this->info; - } + /** + * Returns the request info + * + * @return array + */ + public function info(): array + { + return $this->info; + } - /** - * Decode the response content - * - * @param bool $array decode as array or object - * @return array|\stdClass - */ - public function json(bool $array = true) - { - return json_decode($this->content(), $array); - } + /** + * Decode the response content + * + * @param bool $array decode as array or object + * @return array|\stdClass + */ + public function json(bool $array = true) + { + return json_decode($this->content(), $array); + } - /** - * Returns the request method - * - * @return string - */ - public function method(): string - { - return $this->options['method']; - } + /** + * Returns the request method + * + * @return string + */ + public function method(): string + { + return $this->options['method']; + } - /** - * Returns all options which have been - * set for the current request - * - * @return array - */ - public function options(): array - { - return $this->options; - } + /** + * Returns all options which have been + * set for the current request + * + * @return array + */ + public function options(): array + { + return $this->options; + } - /** - * Internal method to handle post field data - * - * @param mixed $data - * @return mixed - */ - protected function postfields($data) - { - if (is_object($data) || is_array($data)) { - return http_build_query($data); - } else { - return $data; - } - } + /** + * Internal method to handle post field data + * + * @param mixed $data + * @return mixed + */ + protected function postfields($data) + { + if (is_object($data) || is_array($data)) { + return http_build_query($data); + } else { + return $data; + } + } - /** - * Static method to init this class and send a request - * - * @param string $url - * @param array $params - * @return static - */ - public static function request(string $url, array $params = []) - { - return new static($url, $params); - } + /** + * Static method to init this class and send a request + * + * @param string $url + * @param array $params + * @return static + */ + public static function request(string $url, array $params = []) + { + return new static($url, $params); + } - /** - * Returns the request Url - * - * @return string - */ - public function url(): string - { - return $this->options['url']; - } + /** + * Returns the request Url + * + * @return string + */ + public function url(): string + { + return $this->options['url']; + } } diff --git a/kirby/src/Http/Request.php b/kirby/src/Http/Request.php index ee859d8..0b6a897 100644 --- a/kirby/src/Http/Request.php +++ b/kirby/src/Http/Request.php @@ -2,8 +2,7 @@ namespace Kirby\Http; -use Kirby\Http\Request\Auth\BasicAuth; -use Kirby\Http\Request\Auth\BearerAuth; +use Kirby\Cms\App; use Kirby\Http\Request\Body; use Kirby\Http\Request\Files; use Kirby\Http\Request\Query; @@ -23,390 +22,425 @@ use Kirby\Toolkit\Str; */ class Request { - /** - * The auth object if available - * - * @var BearerAuth|BasicAuth|false|null - */ - protected $auth; + public static $authTypes = [ + 'basic' => 'Kirby\Http\Request\Auth\BasicAuth', + 'bearer' => 'Kirby\Http\Request\Auth\BearerAuth', + 'session' => 'Kirby\Http\Request\Auth\SessionAuth', + ]; - /** - * The Body object is a wrapper around - * the request body, which parses the contents - * of the body and provides an API to fetch - * particular parts of the body - * - * Examples: - * - * `$request->body()->get('foo')` - * - * @var Body - */ - protected $body; + /** + * The auth object if available + * + * @var \Kirby\Http\Request\Auth|false|null + */ + protected $auth; - /** - * The Files object is a wrapper around - * the $_FILES global. It sanitizes the - * $_FILES array and provides an API to fetch - * individual files by key - * - * Examples: - * - * `$request->files()->get('upload')['size']` - * `$request->file('upload')['size']` - * - * @var Files - */ - protected $files; + /** + * The Body object is a wrapper around + * the request body, which parses the contents + * of the body and provides an API to fetch + * particular parts of the body + * + * Examples: + * + * `$request->body()->get('foo')` + * + * @var Body + */ + protected $body; - /** - * The Method type - * - * @var string - */ - protected $method; + /** + * The Files object is a wrapper around + * the $_FILES global. It sanitizes the + * $_FILES array and provides an API to fetch + * individual files by key + * + * Examples: + * + * `$request->files()->get('upload')['size']` + * `$request->file('upload')['size']` + * + * @var Files + */ + protected $files; - /** - * All options that have been passed to - * the request in the constructor - * - * @var array - */ - protected $options; + /** + * The Method type + * + * @var string + */ + protected $method; - /** - * The Query object is a wrapper around - * the URL query string, which parses the - * string and provides a clean API to fetch - * particular parts of the query - * - * Examples: - * - * `$request->query()->get('foo')` - * - * @var Query - */ - protected $query; + /** + * All options that have been passed to + * the request in the constructor + * + * @var array + */ + protected $options; - /** - * Request URL object - * - * @var Uri - */ - protected $url; + /** + * The Query object is a wrapper around + * the URL query string, which parses the + * string and provides a clean API to fetch + * particular parts of the query + * + * Examples: + * + * `$request->query()->get('foo')` + * + * @var Query + */ + protected $query; - /** - * Creates a new Request object - * You can either pass your own request - * data via the $options array or use - * the data from the incoming request. - * - * @param array $options - */ - public function __construct(array $options = []) - { - $this->options = $options; - $this->method = $this->detectRequestMethod($options['method'] ?? null); + /** + * Request URL object + * + * @var Uri + */ + protected $url; - if (isset($options['body']) === true) { - $this->body = new Body($options['body']); - } + /** + * Creates a new Request object + * You can either pass your own request + * data via the $options array or use + * the data from the incoming request. + * + * @param array $options + */ + public function __construct(array $options = []) + { + $this->options = $options; + $this->method = $this->detectRequestMethod($options['method'] ?? null); - if (isset($options['files']) === true) { - $this->files = new Files($options['files']); - } + if (isset($options['body']) === true) { + $this->body = is_a($options['body'], Body::class) ? $options['body'] : new Body($options['body']); + } - if (isset($options['query']) === true) { - $this->query = new Query($options['query']); - } + if (isset($options['files']) === true) { + $this->files = is_a($options['files'], Files::class) ? $options['files'] : new Files($options['files']); + } - if (isset($options['url']) === true) { - $this->url = new Uri($options['url']); - } - } + if (isset($options['query']) === true) { + $this->query = is_a($options['query'], Query::class) === true ? $options['query'] : new Query($options['query']); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return [ - 'body' => $this->body(), - 'files' => $this->files(), - 'method' => $this->method(), - 'query' => $this->query(), - 'url' => $this->url()->toString() - ]; - } + if (isset($options['url']) === true) { + $this->url = is_a($options['url'], Uri::class) === true ? $options['url'] : new Uri($options['url']); + } + } - /** - * Returns the Auth object if authentication is set - * - * @return \Kirby\Http\Request\Auth\BasicAuth|\Kirby\Http\Request\Auth\BearerAuth|null - */ - public function auth() - { - if ($this->auth !== null) { - return $this->auth; - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return [ + 'body' => $this->body(), + 'files' => $this->files(), + 'method' => $this->method(), + 'query' => $this->query(), + 'url' => $this->url()->toString() + ]; + } - if ($auth = $this->options['auth'] ?? $this->header('authorization')) { - $type = Str::before($auth, ' '); - $token = Str::after($auth, ' '); - $class = 'Kirby\\Http\\Request\\Auth\\' . ucfirst($type) . 'Auth'; + /** + * Returns the Auth object if authentication is set + * + * @return \Kirby\Http\Request\Auth|null + */ + public function auth() + { + if ($this->auth !== null) { + return $this->auth; + } - if (class_exists($class) === false) { - return $this->auth = false; - } + // lazily request the instance for non-CMS use cases + $kirby = App::instance(null, true); - return $this->auth = new $class($token); - } + // tell the CMS responder that the response relies on + // the `Authorization` header and its value (even if + // the header isn't set in the current request); + // this ensures that the response is only cached for + // unauthenticated visitors; + // https://github.com/getkirby/kirby/issues/4423#issuecomment-1166300526 + if ($kirby) { + $kirby->response()->usesAuth(true); + } - return $this->auth = false; - } + if ($auth = $this->options['auth'] ?? $this->header('authorization')) { + $type = Str::lower(Str::before($auth, ' ')); + $data = Str::after($auth, ' '); - /** - * Returns the Body object - * - * @return \Kirby\Http\Request\Body - */ - public function body() - { - return $this->body ??= new Body(); - } + $class = static::$authTypes[$type] ?? null; + if (!$class || class_exists($class) === false) { + return $this->auth = false; + } - /** - * Checks if the request has been made from the command line - * - * @return bool - */ - public function cli(): bool - { - return Server::cli(); - } + $object = new $class($data); - /** - * Returns a CSRF token if stored in a header or the query - * - * @return string|null - */ - public function csrf(): ?string - { - return $this->header('x-csrf') ?? $this->query()->get('csrf'); - } + return $this->auth = $object; + } - /** - * Returns the request input as array - * - * @return array - */ - public function data(): array - { - return array_merge($this->body()->toArray(), $this->query()->toArray()); - } + return $this->auth = false; + } - /** - * Detect the request method from various - * options: given method, query string, server vars - * - * @param string $method - * @return string - */ - public function detectRequestMethod(string $method = null): string - { - // all possible methods - $methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']; + /** + * Returns the Body object + * + * @return \Kirby\Http\Request\Body + */ + public function body() + { + return $this->body ??= new Body(); + } - // the request method can be overwritten with a header - $methodOverride = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ?? ''); + /** + * Checks if the request has been made from the command line + * + * @return bool + */ + public function cli(): bool + { + return $this->options['cli'] ?? (new Environment())->cli(); + } - if ($method === null && in_array($methodOverride, $methods) === true) { - $method = $methodOverride; - } + /** + * Returns a CSRF token if stored in a header or the query + * + * @return string|null + */ + public function csrf(): ?string + { + return $this->header('x-csrf') ?? $this->query()->get('csrf'); + } - // final chain of options to detect the method - $method = $method ?? $_SERVER['REQUEST_METHOD'] ?? 'GET'; + /** + * Returns the request input as array + * + * @return array + */ + public function data(): array + { + return array_merge($this->body()->toArray(), $this->query()->toArray()); + } - // uppercase the shit out of it - $method = strtoupper($method); + /** + * Detect the request method from various + * options: given method, query string, server vars + * + * @param string $method + * @return string + */ + public function detectRequestMethod(string $method = null): string + { + // all possible methods + $methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH']; - // sanitize the method - if (in_array($method, $methods) === false) { - $method = 'GET'; - } + // the request method can be overwritten with a header + $methodOverride = strtoupper(Environment::getGlobally('HTTP_X_HTTP_METHOD_OVERRIDE', '')); - return $method; - } + if ($method === null && in_array($methodOverride, $methods) === true) { + $method = $methodOverride; + } - /** - * Returns the domain - * - * @return string - */ - public function domain(): string - { - return $this->url()->domain(); - } + // final chain of options to detect the method + $method = $method ?? Environment::getGlobally('REQUEST_METHOD', 'GET'); - /** - * Fetches a single file array - * from the Files object by key - * - * @param string $key - * @return array|null - */ - public function file(string $key) - { - return $this->files()->get($key); - } + // uppercase the shit out of it + $method = strtoupper($method); - /** - * Returns the Files object - * - * @return \Kirby\Cms\Files - */ - public function files() - { - return $this->files ??= new Files(); - } + // sanitize the method + if (in_array($method, $methods) === false) { + $method = 'GET'; + } - /** - * Returns any data field from the request - * if it exists - * - * @param string|null|array $key - * @param mixed $fallback - * @return mixed - */ - public function get($key = null, $fallback = null) - { - return A::get($this->data(), $key, $fallback); - } + return $method; + } - /** - * Returns a header by key if it exists - * - * @param string $key - * @param mixed $fallback - * @return mixed - */ - public function header(string $key, $fallback = null) - { - $headers = array_change_key_case($this->headers()); - return $headers[strtolower($key)] ?? $fallback; - } + /** + * Returns the domain + * + * @return string + */ + public function domain(): string + { + return $this->url()->domain(); + } - /** - * Return all headers with polyfill for - * missing getallheaders function - * - * @return array - */ - public function headers(): array - { - $headers = []; + /** + * Fetches a single file array + * from the Files object by key + * + * @param string $key + * @return array|null + */ + public function file(string $key) + { + return $this->files()->get($key); + } - foreach ($_SERVER as $key => $value) { - if (substr($key, 0, 5) !== 'HTTP_' && substr($key, 0, 14) !== 'REDIRECT_HTTP_') { - continue; - } + /** + * Returns the Files object + * + * @return \Kirby\Cms\Files + */ + public function files() + { + return $this->files ??= new Files(); + } - // remove HTTP_ - $key = str_replace(['REDIRECT_HTTP_', 'HTTP_'], '', $key); + /** + * Returns any data field from the request + * if it exists + * + * @param string|null|array $key + * @param mixed $fallback + * @return mixed + */ + public function get($key = null, $fallback = null) + { + return A::get($this->data(), $key, $fallback); + } - // convert to lowercase - $key = strtolower($key); + /** + * Returns whether the request contains + * the `Authorization` header + * @since 3.7.0 + * + * @return bool + */ + public function hasAuth(): bool + { + $header = $this->options['auth'] ?? $this->header('authorization'); - // replace _ with spaces - $key = str_replace('_', ' ', $key); + return $header !== null; + } - // uppercase first char in each word - $key = ucwords($key); + /** + * Returns a header by key if it exists + * + * @param string $key + * @param mixed $fallback + * @return mixed + */ + public function header(string $key, $fallback = null) + { + $headers = array_change_key_case($this->headers()); + return $headers[strtolower($key)] ?? $fallback; + } - // convert spaces to dashes - $key = str_replace(' ', '-', $key); + /** + * Return all headers with polyfill for + * missing getallheaders function + * + * @return array + */ + public function headers(): array + { + $headers = []; - $headers[$key] = $value; - } + foreach (Environment::getGlobally() as $key => $value) { + if (substr($key, 0, 5) !== 'HTTP_' && substr($key, 0, 14) !== 'REDIRECT_HTTP_') { + continue; + } - return $headers; - } + // remove HTTP_ + $key = str_replace(['REDIRECT_HTTP_', 'HTTP_'], '', $key); - /** - * Checks if the given method name - * matches the name of the request method. - * - * @param string $method - * @return bool - */ - public function is(string $method): bool - { - return strtoupper($this->method) === strtoupper($method); - } + // convert to lowercase + $key = strtolower($key); - /** - * Returns the request method - * - * @return string - */ - public function method(): string - { - return $this->method; - } + // replace _ with spaces + $key = str_replace('_', ' ', $key); - /** - * Shortcut to the Params object - */ - public function params() - { - return $this->url()->params(); - } + // uppercase first char in each word + $key = ucwords($key); - /** - * Shortcut to the Path object - */ - public function path() - { - return $this->url()->path(); - } + // convert spaces to dashes + $key = str_replace(' ', '-', $key); - /** - * Returns the Query object - * - * @return \Kirby\Http\Request\Query - */ - public function query() - { - return $this->query ??= new Query(); - } + $headers[$key] = $value; + } - /** - * Checks for a valid SSL connection - * - * @return bool - */ - public function ssl(): bool - { - return $this->url()->scheme() === 'https'; - } + return $headers; + } - /** - * Returns the current Uri object. - * If you pass props you can safely modify - * the Url with new parameters without destroying - * the original object. - * - * @param array $props - * @return \Kirby\Http\Uri - */ - public function url(array $props = null) - { - if ($props !== null) { - return $this->url()->clone($props); - } + /** + * Checks if the given method name + * matches the name of the request method. + * + * @param string $method + * @return bool + */ + public function is(string $method): bool + { + return strtoupper($this->method) === strtoupper($method); + } - return $this->url ??= Uri::current(); - } + /** + * Returns the request method + * + * @return string + */ + public function method(): string + { + return $this->method; + } + + /** + * Shortcut to the Params object + */ + public function params() + { + return $this->url()->params(); + } + + /** + * Shortcut to the Path object + */ + public function path() + { + return $this->url()->path(); + } + + /** + * Returns the Query object + * + * @return \Kirby\Http\Request\Query + */ + public function query() + { + return $this->query ??= new Query(); + } + + /** + * Checks for a valid SSL connection + * + * @return bool + */ + public function ssl(): bool + { + return $this->url()->scheme() === 'https'; + } + + /** + * Returns the current Uri object. + * If you pass props you can safely modify + * the Url with new parameters without destroying + * the original object. + * + * @param array $props + * @return \Kirby\Http\Uri + */ + public function url(array $props = null) + { + if ($props !== null) { + return $this->url()->clone($props); + } + + return $this->url ??= Uri::current(); + } } diff --git a/kirby/src/Http/Request/Auth.php b/kirby/src/Http/Request/Auth.php new file mode 100644 index 0000000..032ca11 --- /dev/null +++ b/kirby/src/Http/Request/Auth.php @@ -0,0 +1,61 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +abstract class Auth +{ + /** + * Raw authentication data after the first space + * in the `Authorization` header + * + * @var string + */ + protected $data; + + /** + * Constructor + * + * @param string $data + */ + public function __construct(string $data) + { + $this->data = $data; + } + + /** + * Converts the object to a string + * + * @return string + */ + public function __toString(): string + { + return ucfirst($this->type()) . ' ' . $this->data(); + } + + /** + * Returns the raw authentication data after the + * first space in the `Authorization` header + * + * @return string + */ + public function data(): string + { + return $this->data; + } + + /** + * Returns the name of the auth type (lowercase) + * + * @return string + */ + abstract public function type(): string; +} diff --git a/kirby/src/Http/Request/Auth/BasicAuth.php b/kirby/src/Http/Request/Auth/BasicAuth.php index 4df6e8f..3d6e70e 100644 --- a/kirby/src/Http/Request/Auth/BasicAuth.php +++ b/kirby/src/Http/Request/Auth/BasicAuth.php @@ -2,77 +2,84 @@ namespace Kirby\Http\Request\Auth; +use Kirby\Http\Request\Auth; use Kirby\Toolkit\Str; /** - * Basic Authentication + * HTTP basic authentication data + * + * @package Kirby Http + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT */ -class BasicAuth extends BearerAuth +class BasicAuth extends Auth { - /** - * @var string - */ - protected $credentials; + /** + * @var string + */ + protected $credentials; - /** - * @var string - */ - protected $password; + /** + * @var string + */ + protected $password; - /** - * @var string - */ - protected $username; + /** + * @var string + */ + protected $username; - /** - * @param string $token - */ - public function __construct(string $token) - { - parent::__construct($token); + /** + * @param string $token + */ + public function __construct(string $data) + { + parent::__construct($data); - $this->credentials = base64_decode($token); - $this->username = Str::before($this->credentials, ':'); - $this->password = Str::after($this->credentials, ':'); - } + $this->credentials = base64_decode($data); + $this->username = Str::before($this->credentials, ':'); + $this->password = Str::after($this->credentials, ':'); + } - /** - * Returns the entire unencoded credentials string - * - * @return string - */ - public function credentials(): string - { - return $this->credentials; - } + /** + * Returns the entire unencoded credentials string + * + * @return string + */ + public function credentials(): string + { + return $this->credentials; + } - /** - * Returns the password - * - * @return string|null - */ - public function password(): ?string - { - return $this->password; - } + /** + * Returns the password + * + * @return string|null + */ + public function password(): ?string + { + return $this->password; + } - /** - * Returns the authentication type - * - * @return string - */ - public function type(): string - { - return 'basic'; - } + /** + * Returns the authentication type + * + * @return string + */ + public function type(): string + { + return 'basic'; + } - /** - * Returns the username - * - * @return string|null - */ - public function username(): ?string - { - return $this->username; - } + /** + * Returns the username + * + * @return string|null + */ + public function username(): ?string + { + return $this->username; + } } diff --git a/kirby/src/Http/Request/Auth/BearerAuth.php b/kirby/src/Http/Request/Auth/BearerAuth.php index 2c5b1c2..e287606 100644 --- a/kirby/src/Http/Request/Auth/BearerAuth.php +++ b/kirby/src/Http/Request/Auth/BearerAuth.php @@ -2,53 +2,36 @@ namespace Kirby\Http\Request\Auth; +use Kirby\Http\Request\Auth; + /** - * Bearer Auth + * Bearer token authentication data + * + * @package Kirby Http + * @author Bastian Allgeier + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT */ -class BearerAuth +class BearerAuth extends Auth { - /** - * @var string - */ - protected $token; + /** + * Returns the authentication token + * + * @return string + */ + public function token(): string + { + return $this->data; + } - /** - * Creates a new Bearer Auth object - * - * @param string $token - */ - public function __construct(string $token) - { - $this->token = $token; - } - - /** - * Converts the object to a string - * - * @return string - */ - public function __toString(): string - { - return ucfirst($this->type()) . ' ' . $this->token(); - } - - /** - * Returns the authentication token - * - * @return string - */ - public function token(): string - { - return $this->token; - } - - /** - * Returns the auth type - * - * @return string - */ - public function type(): string - { - return 'bearer'; - } + /** + * Returns the auth type + * + * @return string + */ + public function type(): string + { + return 'bearer'; + } } diff --git a/kirby/src/Http/Request/Auth/SessionAuth.php b/kirby/src/Http/Request/Auth/SessionAuth.php new file mode 100644 index 0000000..1ce29be --- /dev/null +++ b/kirby/src/Http/Request/Auth/SessionAuth.php @@ -0,0 +1,48 @@ + + * @link https://getkirby.com + * @copyright Bastian Allgeier + * @license https://opensource.org/licenses/MIT + */ +class SessionAuth extends Auth +{ + /** + * Tries to return the session object + * + * @return \Kirby\Session\Session + */ + public function session() + { + return App::instance()->sessionHandler()->getManually($this->data); + } + + /** + * Returns the session token + * + * @return string + */ + public function token(): string + { + return $this->data; + } + + /** + * Returns the authentication type + * + * @return string + */ + public function type(): string + { + return 'session'; + } +} diff --git a/kirby/src/Http/Request/Body.php b/kirby/src/Http/Request/Body.php index 3ebecbc..df6f330 100644 --- a/kirby/src/Http/Request/Body.php +++ b/kirby/src/Http/Request/Body.php @@ -16,114 +16,114 @@ namespace Kirby\Http\Request; */ class Body { - use Data; + use Data; - /** - * The raw body content - * - * @var string|array - */ - protected $contents; + /** + * The raw body content + * + * @var string|array + */ + protected $contents; - /** - * The parsed content as array - * - * @var array - */ - protected $data; + /** + * The parsed content as array + * + * @var array + */ + protected $data; - /** - * Creates a new request body object. - * You can pass your own array or string. - * 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 - */ - public function __construct($contents = null) - { - $this->contents = $contents; - } + /** + * Creates a new request body object. + * You can pass your own array or string. + * 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 + */ + public function __construct($contents = null) + { + $this->contents = $contents; + } - /** - * Fetches the raw contents for the body - * or uses the passed contents. - * - * @return string|array - */ - public function contents() - { - if ($this->contents === null) { - if (empty($_POST) === false) { - $this->contents = $_POST; - } else { - $this->contents = file_get_contents('php://input'); - } - } + /** + * Fetches the raw contents for the body + * or uses the passed contents. + * + * @return string|array + */ + public function contents() + { + if ($this->contents === null) { + if (empty($_POST) === false) { + $this->contents = $_POST; + } else { + $this->contents = file_get_contents('php://input'); + } + } - return $this->contents; - } + return $this->contents; + } - /** - * Parses the raw contents once and caches - * the result. The parser will try to convert - * the body with the json decoder first and - * then run parse_str to get some results - * if the json decoder failed. - * - * @return array - */ - public function data(): array - { - if (is_array($this->data) === true) { - return $this->data; - } + /** + * Parses the raw contents once and caches + * the result. The parser will try to convert + * the body with the json decoder first and + * then run parse_str to get some results + * if the json decoder failed. + * + * @return array + */ + public function data(): array + { + if (is_array($this->data) === true) { + return $this->data; + } - $contents = $this->contents(); + $contents = $this->contents(); - // return content which is already in array form - if (is_array($contents) === true) { - return $this->data = $contents; - } + // return content which is already in array form + if (is_array($contents) === true) { + return $this->data = $contents; + } - // try to convert the body from json - $json = json_decode($contents, true); + // try to convert the body from json + $json = json_decode($contents, true); - if (is_array($json) === true) { - return $this->data = $json; - } + if (is_array($json) === true) { + return $this->data = $json; + } - if (strstr($contents, '=') !== false) { - // try to parse the body as query string - parse_str($contents, $parsed); + if (strstr($contents, '=') !== false) { + // try to parse the body as query string + parse_str($contents, $parsed); - if (is_array($parsed)) { - return $this->data = $parsed; - } - } + if (is_array($parsed)) { + return $this->data = $parsed; + } + } - return $this->data = []; - } + return $this->data = []; + } - /** - * Converts the data array back - * to a http query string - * - * @return string - */ - public function toString(): string - { - return http_build_query($this->data()); - } + /** + * Converts the data array back + * to a http query string + * + * @return string + */ + public function toString(): string + { + return http_build_query($this->data()); + } - /** - * Magic string converter - * - * @return string - */ - public function __toString(): string - { - return $this->toString(); - } + /** + * Magic string converter + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } } diff --git a/kirby/src/Http/Request/Data.php b/kirby/src/Http/Request/Data.php index d9c4af8..8826c90 100644 --- a/kirby/src/Http/Request/Data.php +++ b/kirby/src/Http/Request/Data.php @@ -18,67 +18,67 @@ namespace Kirby\Http\Request; */ trait Data { - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * The data provider method has to be - * implemented by each class using this Trait - * and has to return an associative array - * for the get method - * - * @return array - */ - abstract public function data(): array; + /** + * The data provider method has to be + * implemented by each class using this Trait + * and has to return an associative array + * for the get method + * + * @return array + */ + abstract public function data(): array; - /** - * The get method is the heart and soul of this - * Trait. You can use it to fetch a single value - * of the data array by key or multiple values by - * passing an array of keys. - * - * @param string|array $key - * @param mixed|null $default - * @return mixed - */ - public function get($key, $default = null) - { - if (is_array($key) === true) { - $result = []; - foreach ($key as $k) { - $result[$k] = $this->get($k); - } - return $result; - } + /** + * The get method is the heart and soul of this + * Trait. You can use it to fetch a single value + * of the data array by key or multiple values by + * passing an array of keys. + * + * @param string|array $key + * @param mixed|null $default + * @return mixed + */ + public function get($key, $default = null) + { + if (is_array($key) === true) { + $result = []; + foreach ($key as $k) { + $result[$k] = $this->get($k); + } + return $result; + } - return $this->data()[$key] ?? $default; - } + return $this->data()[$key] ?? $default; + } - /** - * Returns the data array. - * This is basically an alias for Data::data() - * - * @return array - */ - public function toArray(): array - { - return $this->data(); - } + /** + * Returns the data array. + * This is basically an alias for Data::data() + * + * @return array + */ + public function toArray(): array + { + return $this->data(); + } - /** - * Converts the data array to json - * - * @return string - */ - public function toJson(): string - { - return json_encode($this->data()); - } + /** + * Converts the data array to json + * + * @return string + */ + public function toJson(): string + { + return json_encode($this->data()); + } } diff --git a/kirby/src/Http/Request/Files.php b/kirby/src/Http/Request/Files.php index 7a515c9..a23263a 100644 --- a/kirby/src/Http/Request/Files.php +++ b/kirby/src/Http/Request/Files.php @@ -17,57 +17,57 @@ namespace Kirby\Http\Request; */ class Files { - use Data; + use Data; - /** - * Sanitized array of all received files - * - * @var array - */ - protected $files; + /** + * Sanitized array of all received files + * + * @var array + */ + protected $files; - /** - * Creates a new Files object - * Pass your own array to mock - * uploads. - * - * @param array|null $files - */ - public function __construct($files = null) - { - if ($files === null) { - $files = $_FILES; - } + /** + * Creates a new Files object + * Pass your own array to mock + * uploads. + * + * @param array|null $files + */ + public function __construct($files = null) + { + if ($files === null) { + $files = $_FILES; + } - $this->files = []; + $this->files = []; - foreach ($files as $key => $file) { - if (is_array($file['name'])) { - foreach ($file['name'] as $i => $name) { - $this->files[$key][] = [ - 'name' => $file['name'][$i] ?? null, - 'type' => $file['type'][$i] ?? null, - 'tmp_name' => $file['tmp_name'][$i] ?? null, - 'error' => $file['error'][$i] ?? null, - 'size' => $file['size'][$i] ?? null, - ]; - } - } else { - $this->files[$key] = $file; - } - } - } + foreach ($files as $key => $file) { + if (is_array($file['name'])) { + foreach ($file['name'] as $i => $name) { + $this->files[$key][] = [ + 'name' => $file['name'][$i] ?? null, + 'type' => $file['type'][$i] ?? null, + 'tmp_name' => $file['tmp_name'][$i] ?? null, + 'error' => $file['error'][$i] ?? null, + 'size' => $file['size'][$i] ?? null, + ]; + } + } else { + $this->files[$key] = $file; + } + } + } - /** - * The data method returns the files - * array. This is only needed to make - * the Data trait work for the Files::get($key) - * method. - * - * @return array - */ - public function data(): array - { - return $this->files; - } + /** + * The data method returns the files + * array. This is only needed to make + * the Data trait work for the Files::get($key) + * method. + * + * @return array + */ + public function data(): array + { + return $this->files; + } } diff --git a/kirby/src/Http/Request/Query.php b/kirby/src/Http/Request/Query.php index 5e681e4..315a683 100644 --- a/kirby/src/Http/Request/Query.php +++ b/kirby/src/Http/Request/Query.php @@ -15,84 +15,84 @@ namespace Kirby\Http\Request; */ class Query { - use Data; + use Data; - /** - * The Query data array - * - * @var array|null - */ - protected $data; + /** + * The Query data array + * + * @var array|null + */ + protected $data; - /** - * Creates a new Query object. - * The passed data can be an array - * or a parsable query string. If - * null is passed, the current Query - * will be taken from $_GET - * - * @param array|string|null $data - */ - public function __construct($data = null) - { - if ($data === null) { - $this->data = $_GET; - } elseif (is_array($data)) { - $this->data = $data; - } else { - parse_str($data, $parsed); - $this->data = $parsed; - } - } + /** + * Creates a new Query object. + * The passed data can be an array + * or a parsable query string. If + * null is passed, the current Query + * will be taken from $_GET + * + * @param array|string|null $data + */ + public function __construct($data = null) + { + if ($data === null) { + $this->data = $_GET; + } elseif (is_array($data)) { + $this->data = $data; + } else { + parse_str($data, $parsed); + $this->data = $parsed; + } + } - /** - * Returns the Query data as array - * - * @return array - */ - public function data(): array - { - return $this->data; - } + /** + * Returns the Query data as array + * + * @return array + */ + public function data(): array + { + return $this->data; + } - /** - * Returns `true` if the request doesn't contain query variables - * - * @return bool - */ - public function isEmpty(): bool - { - return empty($this->data) === true; - } + /** + * Returns `true` if the request doesn't contain query variables + * + * @return bool + */ + public function isEmpty(): bool + { + return empty($this->data) === true; + } - /** - * Returns `true` if the request contains query variables - * - * @return bool - */ - public function isNotEmpty(): bool - { - return empty($this->data) === false; - } + /** + * Returns `true` if the request contains query variables + * + * @return bool + */ + public function isNotEmpty(): bool + { + return empty($this->data) === false; + } - /** - * Converts the query data array - * back to a query string - * - * @return string - */ - public function toString(): string - { - return http_build_query($this->data()); - } + /** + * Converts the query data array + * back to a query string + * + * @return string + */ + public function toString(): string + { + return http_build_query($this->data()); + } - /** - * Magic string converter - * - * @return string - */ - public function __toString(): string - { - return $this->toString(); - } + /** + * Magic string converter + * + * @return string + */ + public function __toString(): string + { + return $this->toString(); + } } diff --git a/kirby/src/Http/Response.php b/kirby/src/Http/Response.php index d552839..6b1e487 100644 --- a/kirby/src/Http/Response.php +++ b/kirby/src/Http/Response.php @@ -19,299 +19,316 @@ use Throwable; */ class Response { - /** - * Store for all registered headers, - * which will be sent with the response - * - * @var array - */ - protected $headers = []; + /** + * Store for all registered headers, + * which will be sent with the response + * + * @var array + */ + protected $headers = []; - /** - * The response body - * - * @var string - */ - protected $body; + /** + * The response body + * + * @var string + */ + protected $body; - /** - * The HTTP response code - * - * @var int - */ - protected $code; + /** + * The HTTP response code + * + * @var int + */ + protected $code; - /** - * The content type for the response - * - * @var string - */ - protected $type; + /** + * The content type for the response + * + * @var string + */ + protected $type; - /** - * The content type charset - * - * @var string - */ - protected $charset = 'UTF-8'; + /** + * The content type charset + * + * @var string + */ + protected $charset = 'UTF-8'; - /** - * Creates a new response object - * - * @param string $body - * @param string $type - * @param int $code - * @param array $headers - * @param string $charset - */ - public function __construct($body = '', ?string $type = null, ?int $code = null, ?array $headers = null, ?string $charset = null) - { - // array construction - if (is_array($body) === true) { - $params = $body; - $body = $params['body'] ?? ''; - $type = $params['type'] ?? $type; - $code = $params['code'] ?? $code; - $headers = $params['headers'] ?? $headers; - $charset = $params['charset'] ?? $charset; - } + /** + * Creates a new response object + * + * @param string $body + * @param string $type + * @param int $code + * @param array $headers + * @param string $charset + */ + public function __construct($body = '', ?string $type = null, ?int $code = null, ?array $headers = null, ?string $charset = null) + { + // array construction + if (is_array($body) === true) { + $params = $body; + $body = $params['body'] ?? ''; + $type = $params['type'] ?? $type; + $code = $params['code'] ?? $code; + $headers = $params['headers'] ?? $headers; + $charset = $params['charset'] ?? $charset; + } - // regular construction - $this->body = $body; - $this->type = $type ?? 'text/html'; - $this->code = $code ?? 200; - $this->headers = $headers ?? []; - $this->charset = $charset ?? 'UTF-8'; + // regular construction + $this->body = $body; + $this->type = $type ?? 'text/html'; + $this->code = $code ?? 200; + $this->headers = $headers ?? []; + $this->charset = $charset ?? 'UTF-8'; - // automatic mime type detection - if (strpos($this->type, '/') === false) { - $this->type = F::extensionToMime($this->type) ?? 'text/html'; - } - } + // automatic mime type detection + if (strpos($this->type, '/') === false) { + $this->type = F::extensionToMime($this->type) ?? 'text/html'; + } + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * Makes it possible to convert the - * entire response object to a string - * to send the headers and print the body - * - * @return string - */ - public function __toString(): string - { - try { - return $this->send(); - } catch (Throwable $e) { - return ''; - } - } + /** + * Makes it possible to convert the + * entire response object to a string + * to send the headers and print the body + * + * @return string + */ + public function __toString(): string + { + try { + return $this->send(); + } catch (Throwable $e) { + return ''; + } + } - /** - * Getter for the body - * - * @return string - */ - public function body(): string - { - return $this->body; - } + /** + * Getter for the body + * + * @return string + */ + public function body(): string + { + return $this->body; + } - /** - * Getter for the content type charset - * - * @return string - */ - public function charset(): string - { - return $this->charset; - } + /** + * Getter for the content type charset + * + * @return string + */ + public function charset(): string + { + return $this->charset; + } - /** - * Getter for the HTTP status code - * - * @return int - */ - public function code(): int - { - return $this->code; - } + /** + * Getter for the HTTP status code + * + * @return int + */ + public function code(): int + { + return $this->code; + } - /** - * Creates a response that triggers - * a file download for the given file - * - * @param string $file - * @param string $filename - * @param array $props Custom overrides for response props (e.g. headers) - * @return static - */ - public static function download(string $file, string $filename = null, array $props = []) - { - if (file_exists($file) === false) { - throw new Exception('The file could not be found'); - } + /** + * Creates a response that triggers + * a file download for the given file + * + * @param string $file + * @param string $filename + * @param array $props Custom overrides for response props (e.g. headers) + * @return static + */ + public static function download(string $file, string $filename = null, array $props = []) + { + if (file_exists($file) === false) { + throw new Exception('The file could not be found'); + } - $filename ??= basename($file); - $modified = filemtime($file); - $body = file_get_contents($file); - $size = strlen($body); + $filename ??= basename($file); + $modified = filemtime($file); + $body = file_get_contents($file); + $size = strlen($body); - $props = array_replace_recursive([ - 'body' => $body, - 'type' => F::mime($file), - 'headers' => [ - 'Pragma' => 'public', - 'Cache-Control' => 'no-cache, no-store, must-revalidate', - 'Last-Modified' => gmdate('D, d M Y H:i:s', $modified) . ' GMT', - 'Content-Disposition' => 'attachment; filename="' . $filename . '"', - 'Content-Transfer-Encoding' => 'binary', - 'Content-Length' => $size, - 'Connection' => 'close' - ] - ], $props); + $props = array_replace_recursive([ + 'body' => $body, + 'type' => F::mime($file), + 'headers' => [ + 'Pragma' => 'public', + 'Cache-Control' => 'no-cache, no-store, must-revalidate', + 'Last-Modified' => gmdate('D, d M Y H:i:s', $modified) . ' GMT', + 'Content-Disposition' => 'attachment; filename="' . $filename . '"', + 'Content-Transfer-Encoding' => 'binary', + 'Content-Length' => $size, + 'Connection' => 'close' + ] + ], $props); - return new static($props); - } + return new static($props); + } - /** - * Creates a response for a file and - * sends the file content to the browser - * - * @param string $file - * @param array $props Custom overrides for response props (e.g. headers) - * @return static - */ - public static function file(string $file, array $props = []) - { - $props = array_merge([ - 'body' => F::read($file), - 'type' => F::extensionToMime(F::extension($file)) - ], $props); + /** + * Creates a response for a file and + * sends the file content to the browser + * + * @param string $file + * @param array $props Custom overrides for response props (e.g. headers) + * @return static + */ + public static function file(string $file, array $props = []) + { + $props = array_merge([ + 'body' => F::read($file), + 'type' => F::extensionToMime(F::extension($file)) + ], $props); - return new static($props); - } + return new static($props); + } - /** - * Getter for single headers - * - * @param string $key Name of the header - * @return string|null - */ - public function header(string $key): ?string - { - return $this->headers[$key] ?? null; - } - /** - * Getter for all headers - * - * @return array - */ - public function headers(): array - { - return $this->headers; - } + /** + * Redirects to the given Urls + * Urls can be relative or absolute. + * @since 3.7.0 + * + * @param string $url + * @param int $code + * @return void + * + * @codeCoverageIgnore + */ + public static function go(string $url = '/', int $code = 302) + { + die(static::redirect($url, $code)); + } - /** - * Creates a json response with appropriate - * header and automatic conversion of arrays. - * - * @param string|array $body - * @param int $code - * @param bool $pretty - * @param array $headers - * @return static - */ - public static function json($body = '', ?int $code = null, ?bool $pretty = null, array $headers = []) - { - if (is_array($body) === true) { - $body = json_encode($body, $pretty === true ? JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES : 0); - } + /** + * Getter for single headers + * + * @param string $key Name of the header + * @return string|null + */ + public function header(string $key): ?string + { + return $this->headers[$key] ?? null; + } - return new static([ - 'body' => $body, - 'code' => $code, - 'type' => 'application/json', - 'headers' => $headers - ]); - } + /** + * Getter for all headers + * + * @return array + */ + public function headers(): array + { + return $this->headers; + } - /** - * Creates a redirect response, - * which will send the visitor to the - * given location. - * - * @param string $location - * @param int $code - * @return static - */ - public static function redirect(string $location = '/', int $code = 302) - { - return new static([ - 'code' => $code, - 'headers' => [ - 'Location' => Url::unIdn($location) - ] - ]); - } + /** + * Creates a json response with appropriate + * header and automatic conversion of arrays. + * + * @param string|array $body + * @param int $code + * @param bool $pretty + * @param array $headers + * @return static + */ + public static function json($body = '', ?int $code = null, ?bool $pretty = null, array $headers = []) + { + if (is_array($body) === true) { + $body = json_encode($body, $pretty === true ? JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES : 0); + } - /** - * Sends all registered headers and - * returns the response body - * - * @return string - */ - public function send(): string - { - // send the status response code - http_response_code($this->code()); + return new static([ + 'body' => $body, + 'code' => $code, + 'type' => 'application/json', + 'headers' => $headers + ]); + } - // send all custom headers - foreach ($this->headers() as $key => $value) { - header($key . ': ' . $value); - } + /** + * Creates a redirect response, + * which will send the visitor to the + * given location. + * + * @param string $location + * @param int $code + * @return static + */ + public static function redirect(string $location = '/', int $code = 302) + { + return new static([ + 'code' => $code, + 'headers' => [ + 'Location' => Url::unIdn($location) + ] + ]); + } - // send the content type header - header('Content-Type:' . $this->type() . '; charset=' . $this->charset()); + /** + * Sends all registered headers and + * returns the response body + * + * @return string + */ + public function send(): string + { + // send the status response code + http_response_code($this->code()); - // print the response body - return $this->body(); - } + // send all custom headers + foreach ($this->headers() as $key => $value) { + header($key . ': ' . $value); + } - /** - * Converts all relevant response attributes - * to an associative array for debugging, - * testing or whatever. - * - * @return array - */ - public function toArray(): array - { - return [ - 'type' => $this->type(), - 'charset' => $this->charset(), - 'code' => $this->code(), - 'headers' => $this->headers(), - 'body' => $this->body() - ]; - } + // send the content type header + header('Content-Type:' . $this->type() . '; charset=' . $this->charset()); - /** - * Getter for the content type - * - * @return string - */ - public function type(): string - { - return $this->type; - } + // print the response body + return $this->body(); + } + + /** + * Converts all relevant response attributes + * to an associative array for debugging, + * testing or whatever. + * + * @return array + */ + public function toArray(): array + { + return [ + 'type' => $this->type(), + 'charset' => $this->charset(), + 'code' => $this->code(), + 'headers' => $this->headers(), + 'body' => $this->body() + ]; + } + + /** + * Getter for the content type + * + * @return string + */ + public function type(): string + { + return $this->type; + } } diff --git a/kirby/src/Http/Route.php b/kirby/src/Http/Route.php index bfad0c9..007e481 100644 --- a/kirby/src/Http/Route.php +++ b/kirby/src/Http/Route.php @@ -13,218 +13,218 @@ use Closure; */ class Route { - /** - * The callback action function - * - * @var Closure - */ - protected $action; + /** + * The callback action function + * + * @var Closure + */ + protected $action; - /** - * Listed of parsed arguments - * - * @var array - */ - protected $arguments = []; + /** + * Listed of parsed arguments + * + * @var array + */ + protected $arguments = []; - /** - * An array of all passed attributes - * - * @var array - */ - protected $attributes = []; + /** + * An array of all passed attributes + * + * @var array + */ + protected $attributes = []; - /** - * The registered request method - * - * @var string - */ - protected $method; + /** + * The registered request method + * + * @var string + */ + protected $method; - /** - * The registered pattern - * - * @var string - */ - protected $pattern; + /** + * The registered pattern + * + * @var string + */ + protected $pattern; - /** - * Wildcards, which can be used in - * Route patterns to make regular expressions - * a little more human - * - * @var array - */ - protected $wildcards = [ - 'required' => [ - '(:num)' => '(-?[0-9]+)', - '(:alpha)' => '([a-zA-Z]+)', - '(:alphanum)' => '([a-zA-Z0-9]+)', - '(:any)' => '([a-zA-Z0-9\.\-_%= \+\@\(\)]+)', - '(:all)' => '(.*)', - ], - 'optional' => [ - '/(:num?)' => '(?:/(-?[0-9]+)', - '/(:alpha?)' => '(?:/([a-zA-Z]+)', - '/(:alphanum?)' => '(?:/([a-zA-Z0-9]+)', - '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%= \+\@\(\)]+)', - '/(:all?)' => '(?:/(.*)', - ], - ]; + /** + * Wildcards, which can be used in + * Route patterns to make regular expressions + * a little more human + * + * @var array + */ + protected $wildcards = [ + 'required' => [ + '(:num)' => '(-?[0-9]+)', + '(:alpha)' => '([a-zA-Z]+)', + '(:alphanum)' => '([a-zA-Z0-9]+)', + '(:any)' => '([a-zA-Z0-9\.\-_%= \+\@\(\)]+)', + '(:all)' => '(.*)', + ], + 'optional' => [ + '/(:num?)' => '(?:/(-?[0-9]+)', + '/(:alpha?)' => '(?:/([a-zA-Z]+)', + '/(:alphanum?)' => '(?:/([a-zA-Z0-9]+)', + '/(:any?)' => '(?:/([a-zA-Z0-9\.\-_%= \+\@\(\)]+)', + '/(:all?)' => '(?:/(.*)', + ], + ]; - /** - * Magic getter for route attributes - * - * @param string $key - * @param array $arguments - * @return mixed - */ - public function __call(string $key, array $arguments = null) - { - return $this->attributes[$key] ?? null; - } + /** + * Magic getter for route attributes + * + * @param string $key + * @param array $arguments + * @return mixed + */ + public function __call(string $key, array $arguments = null) + { + return $this->attributes[$key] ?? null; + } - /** - * Creates a new Route object for the given - * pattern(s), method(s) and the callback action - * - * @param string|array $pattern - * @param string|array $method - * @param Closure $action - * @param array $attributes - */ - public function __construct($pattern, $method, Closure $action, array $attributes = []) - { - $this->action = $action; - $this->attributes = $attributes; - $this->method = $method; - $this->pattern = $this->regex(ltrim($pattern, '/')); - } + /** + * Creates a new Route object for the given + * pattern(s), method(s) and the callback action + * + * @param string|array $pattern + * @param string|array $method + * @param Closure $action + * @param array $attributes + */ + public function __construct($pattern, $method, Closure $action, array $attributes = []) + { + $this->action = $action; + $this->attributes = $attributes; + $this->method = $method; + $this->pattern = $this->regex(ltrim($pattern, '/')); + } - /** - * Getter for the action callback - * - * @return Closure - */ - public function action() - { - return $this->action; - } + /** + * Getter for the action callback + * + * @return Closure + */ + public function action() + { + return $this->action; + } - /** - * Returns all parsed arguments - * - * @return array - */ - public function arguments(): array - { - return $this->arguments; - } + /** + * Returns all parsed arguments + * + * @return array + */ + public function arguments(): array + { + return $this->arguments; + } - /** - * Getter for additional attributes - * - * @return array - */ - public function attributes(): array - { - return $this->attributes; - } + /** + * Getter for additional attributes + * + * @return array + */ + public function attributes(): array + { + return $this->attributes; + } - /** - * Getter for the method - * - * @return string - */ - public function method(): string - { - return $this->method; - } + /** + * Getter for the method + * + * @return string + */ + public function method(): string + { + return $this->method; + } - /** - * Returns the route name if set - * - * @return string|null - */ - public function name(): ?string - { - return $this->attributes['name'] ?? null; - } + /** + * Returns the route name if set + * + * @return string|null + */ + public function name(): ?string + { + return $this->attributes['name'] ?? null; + } - /** - * Throws a specific exception to tell - * the router to jump to the next route - * @since 3.0.3 - * - * @return void - */ - public static function next(): void - { - throw new Exceptions\NextRouteException('next'); - } + /** + * Throws a specific exception to tell + * the router to jump to the next route + * @since 3.0.3 + * + * @return void + */ + public static function next(): void + { + throw new Exceptions\NextRouteException('next'); + } - /** - * Getter for the pattern - * - * @return string - */ - public function pattern(): string - { - return $this->pattern; - } + /** + * Getter for the pattern + * + * @return string + */ + public function pattern(): string + { + return $this->pattern; + } - /** - * Converts the pattern into a full regular - * expression by replacing all the wildcards - * - * @param string $pattern - * @return string - */ - public function regex(string $pattern): string - { - $search = array_keys($this->wildcards['optional']); - $replace = array_values($this->wildcards['optional']); + /** + * Converts the pattern into a full regular + * expression by replacing all the wildcards + * + * @param string $pattern + * @return string + */ + public function regex(string $pattern): string + { + $search = array_keys($this->wildcards['optional']); + $replace = array_values($this->wildcards['optional']); - // For optional parameters, first translate the wildcards to their - // regex equivalent, sans the ")?" ending. We'll add the endings - // back on when we know the replacement count. - $pattern = str_replace($search, $replace, $pattern, $count); + // For optional parameters, first translate the wildcards to their + // regex equivalent, sans the ")?" ending. We'll add the endings + // back on when we know the replacement count. + $pattern = str_replace($search, $replace, $pattern, $count); - if ($count > 0) { - $pattern .= str_repeat(')?', $count); - } + if ($count > 0) { + $pattern .= str_repeat(')?', $count); + } - return strtr($pattern, $this->wildcards['required']); - } + return strtr($pattern, $this->wildcards['required']); + } - /** - * Tries to match the path with the regular expression and - * extracts all arguments for the Route action - * - * @param string $pattern - * @param string $path - * @return array|false - */ - public function parse(string $pattern, string $path) - { - // check for direct matches - if ($pattern === $path) { - return $this->arguments = []; - } + /** + * Tries to match the path with the regular expression and + * extracts all arguments for the Route action + * + * @param string $pattern + * @param string $path + * @return array|false + */ + public function parse(string $pattern, string $path) + { + // check for direct matches + if ($pattern === $path) { + return $this->arguments = []; + } - // We only need to check routes with regular expression since all others - // would have been able to be matched by the search for literal matches - // we just did before we started searching. - if (strpos($pattern, '(') === false) { - return false; - } + // We only need to check routes with regular expression since all others + // would have been able to be matched by the search for literal matches + // we just did before we started searching. + if (strpos($pattern, '(') === false) { + return false; + } - // If we have a match we'll return all results - // from the preg without the full first match. - if (preg_match('#^' . $this->regex($pattern) . '$#u', $path, $parameters)) { - return $this->arguments = array_slice($parameters, 1); - } + // If we have a match we'll return all results + // from the preg without the full first match. + if (preg_match('#^' . $this->regex($pattern) . '$#u', $path, $parameters)) { + return $this->arguments = array_slice($parameters, 1); + } - return false; - } + return false; + } } diff --git a/kirby/src/Http/Router.php b/kirby/src/Http/Router.php index 241bd9b..aceb2b6 100644 --- a/kirby/src/Http/Router.php +++ b/kirby/src/Http/Router.php @@ -16,158 +16,194 @@ use Kirby\Toolkit\A; */ class Router { - public static $beforeEach; - public static $afterEach; + /** + * Hook that is called after each route + * + * @var \Closure + */ + protected $afterEach; - /** - * Store for the current route, - * if one can be found - * - * @var Route|null - */ - protected $route; + /** + * Hook that is called before each route + * + * @var \Closure + */ + protected $beforeEach; - /** - * All registered routes, sorted by - * their request method. This makes - * it faster to find the right route - * later. - * - * @var array - */ - protected $routes = [ - 'GET' => [], - 'HEAD' => [], - 'POST' => [], - 'PUT' => [], - 'DELETE' => [], - 'CONNECT' => [], - 'OPTIONS' => [], - 'TRACE' => [], - 'PATCH' => [], - ]; + /** + * Store for the current route, + * if one can be found + * + * @var \Kirby\Http\Route|null + */ + protected $route; - /** - * Creates a new router object and - * registers all the given routes - * - * @param array $routes - */ - public function __construct(array $routes = []) - { - foreach ($routes as $props) { - if (isset($props['pattern'], $props['action']) === false) { - throw new InvalidArgumentException('Invalid route parameters'); - } + /** + * All registered routes, sorted by + * their request method. This makes + * it faster to find the right route + * later. + * + * @var array + */ + protected $routes = [ + 'GET' => [], + 'HEAD' => [], + 'POST' => [], + 'PUT' => [], + 'DELETE' => [], + 'CONNECT' => [], + 'OPTIONS' => [], + 'TRACE' => [], + 'PATCH' => [], + ]; - $patterns = A::wrap($props['pattern']); - $methods = A::map( - explode('|', strtoupper($props['method'] ?? 'GET')), - 'trim' - ); + /** + * Creates a new router object and + * registers all the given routes + * + * @param array $routes + * @param array $hooks Optional `beforeEach` and `afterEach` hooks + */ + public function __construct(array $routes = [], array $hooks = []) + { + $this->beforeEach = $hooks['beforeEach'] ?? null; + $this->afterEach = $hooks['afterEach'] ?? null; - if ($methods === ['ALL']) { - $methods = array_keys($this->routes); - } + foreach ($routes as $props) { + if (isset($props['pattern'], $props['action']) === false) { + throw new InvalidArgumentException('Invalid route parameters'); + } - foreach ($methods as $method) { - foreach ($patterns as $pattern) { - $this->routes[$method][] = new Route($pattern, $method, $props['action'], $props); - } - } - } - } + $patterns = A::wrap($props['pattern']); + $methods = A::map( + explode('|', strtoupper($props['method'] ?? 'GET')), + 'trim' + ); - /** - * Calls the Router by path and method. - * This will try to find a Route object - * and then call the Route action with - * the appropriate arguments and a Result - * object. - * - * @param string $path - * @param string $method - * @param Closure|null $callback - * @return mixed - */ - public function call(string $path = null, string $method = 'GET', Closure $callback = null) - { - $path ??= ''; - $ignore = []; - $result = null; - $loop = true; + if ($methods === ['ALL']) { + $methods = array_keys($this->routes); + } - while ($loop === true) { - $route = $this->find($path, $method, $ignore); + foreach ($methods as $method) { + foreach ($patterns as $pattern) { + $this->routes[$method][] = new Route( + $pattern, + $method, + $props['action'], + $props + ); + } + } + } + } - if (is_a(static::$beforeEach, 'Closure') === true) { - (static::$beforeEach)($route, $path, $method); - } + /** + * Calls the Router by path and method. + * This will try to find a Route object + * and then call the Route action with + * the appropriate arguments and a Result + * object. + * + * @param string $path + * @param string $method + * @param Closure|null $callback + * @return mixed + */ + public function call(string $path = null, string $method = 'GET', Closure $callback = null) + { + $path ??= ''; + $ignore = []; + $result = null; + $loop = true; - try { - if ($callback) { - $result = $callback($route); - } else { - $result = $route->action()->call($route, ...$route->arguments()); - } + while ($loop === true) { + $route = $this->find($path, $method, $ignore); - $loop = false; - } catch (Exceptions\NextRouteException $e) { - $ignore[] = $route; - } + if (is_a($this->beforeEach, 'Closure') === true) { + ($this->beforeEach)($route, $path, $method); + } - if (is_a(static::$afterEach, 'Closure') === true) { - $final = $loop === false; - $result = (static::$afterEach)($route, $path, $method, $result, $final); - } - } + try { + if ($callback) { + $result = $callback($route); + } else { + $result = $route->action()->call($route, ...$route->arguments()); + } - return $result; - } + $loop = false; + } catch (Exceptions\NextRouteException $e) { + $ignore[] = $route; + } - /** - * Finds a Route object by path and method - * The Route's arguments method is used to - * find matches and return all the found - * arguments in the path. - * - * @param string $path - * @param string $method - * @param array $ignore - * @return \Kirby\Http\Route|null - */ - public function find(string $path, string $method, array $ignore = null) - { - if (isset($this->routes[$method]) === false) { - throw new InvalidArgumentException('Invalid routing method: ' . $method, 400); - } + if (is_a($this->afterEach, 'Closure') === true) { + $final = $loop === false; + $result = ($this->afterEach)($route, $path, $method, $result, $final); + } + } - // remove leading and trailing slashes - $path = trim($path, '/'); + return $result; + } - foreach ($this->routes[$method] as $route) { - $arguments = $route->parse($route->pattern(), $path); + /** + * Creates a micro-router and executes + * the routing action immediately + * @since 3.7.0 + * + * @param string|null $path + * @param string $method + * @param array $routes + * @param \Closure|null $callback + * @return mixed + */ + public static function execute(?string $path = null, string $method = 'GET', array $routes = [], ?Closure $callback = null) + { + return (new static($routes))->call($path, $method, $callback); + } - if ($arguments !== false) { - if (empty($ignore) === true || in_array($route, $ignore) === false) { - return $this->route = $route; - } - } - } + /** + * Finds a Route object by path and method + * The Route's arguments method is used to + * find matches and return all the found + * arguments in the path. + * + * @param string $path + * @param string $method + * @param array $ignore + * @return \Kirby\Http\Route|null + */ + public function find(string $path, string $method, array $ignore = null) + { + if (isset($this->routes[$method]) === false) { + throw new InvalidArgumentException('Invalid routing method: ' . $method, 400); + } - throw new Exception('No route found for path: "' . $path . '" and request method: "' . $method . '"', 404); - } + // remove leading and trailing slashes + $path = trim($path, '/'); - /** - * Returns the current route. - * This will only return something, - * once Router::find() has been called - * and only if a route was found. - * - * @return \Kirby\Http\Route|null - */ - public function route() - { - return $this->route; - } + foreach ($this->routes[$method] as $route) { + $arguments = $route->parse($route->pattern(), $path); + + if ($arguments !== false) { + if (empty($ignore) === true || in_array($route, $ignore) === false) { + return $this->route = $route; + } + } + } + + throw new Exception('No route found for path: "' . $path . '" and request method: "' . $method . '"', 404); + } + + /** + * Returns the current route. + * This will only return something, + * once Router::find() has been called + * and only if a route was found. + * + * @return \Kirby\Http\Route|null + */ + public function route() + { + return $this->route; + } } diff --git a/kirby/src/Http/Server.php b/kirby/src/Http/Server.php index d418273..d16118f 100644 --- a/kirby/src/Http/Server.php +++ b/kirby/src/Http/Server.php @@ -2,7 +2,7 @@ namespace Kirby\Http; -use Kirby\Toolkit\A; +use Kirby\Toolkit\Facade; /** * A set of methods that make it more convenient to get variables @@ -13,336 +13,26 @@ use Kirby\Toolkit\A; * @link https://getkirby.com * @copyright Bastian Allgeier * @license https://opensource.org/licenses/MIT + * @deprecated 3.7.0 Use `Kirby\Http\Environment` instead + * @todo Remove in 3.8.0 */ -class Server +class Server extends Facade { - public const HOST_FROM_SERVER = 1; - public const HOST_FROM_HEADER = 2; - public const HOST_ALLOW_EMPTY = 4; + public const HOST_FROM_SERVER = 1; + public const HOST_FROM_HEADER = 2; + public const HOST_ALLOW_EMPTY = 4; - /** - * Cache for the cli status - * - * @var bool|null - */ - public static $cli; + public static $cli; + public static $hosts; - /** - * List of trusted hosts - * - * @var array - */ - public static $hosts = []; - - /** - * Returns the server's IP address - * - * @return string - */ - public static function address(): string - { - return static::get('SERVER_ADDR', ''); - } - - /** - * Checks if the request is being served by the CLI - * - * @return bool - */ - public static function cli(): bool - { - if (static::$cli !== null) { - return static::$cli; - } - - if (defined('STDIN') === true) { - return static::$cli = true; - } - - $term = getenv('TERM'); - - if (substr(PHP_SAPI, 0, 3) === 'cgi' && $term && $term !== 'unknown') { - return static::$cli = true; - } - - return static::$cli = false; - } - - /** - * Gets a value from the _SERVER array - * - * - * Server::get('document_root'); - * // sample output: /var/www/kirby - * - * Server::get(); - * // returns the whole server array - * - * - * @param mixed $key The key to look for. Pass false or null to - * return the entire server array. - * @param mixed $default Optional default value, which should be - * returned if no element has been found - * @return mixed - */ - public static function get($key = null, $default = null) - { - if ($key === null) { - return $_SERVER; - } - - $key = strtoupper($key); - $value = $_SERVER[$key] ?? $default; - return static::sanitize($key, $value); - } - - /** - * Returns the correct host - * - * @param bool $forwarded Deprecated. Todo: remove in 3.7.0 - * @return string - */ - public static function host(bool $forwarded = false): string - { - $hosts[] = static::get('SERVER_NAME'); - $hosts[] = static::get('SERVER_ADDR'); - - // insecure host parameters are only allowed when hosts - // are validated against set of host patterns - if (empty(static::$hosts) === false) { - $hosts[] = static::get('HTTP_HOST'); - $hosts[] = static::get('HTTP_X_FORWARDED_HOST'); - } - - // remove empty hosts - $hosts = array_filter($hosts); - - foreach ($hosts as $host) { - if (static::isAllowedHost($host) === true) { - return explode(':', $host)[0]; - } - } - - return ''; - } - - /** - * Setter and getter for the the static $hosts property - * - * $hosts = null -> return all defined hosts - * $hosts = Server::HOST_FROM_SERVER -> [] - * $hosts = Server::HOST_FROM_HEADER -> ['*'] - * $hosts = array -> [array of trusted hosts] - * $hosts = string -> [single trusted host] - * - * @param string|array|int|null $hosts - * @return array - */ - public static function hosts($hosts = null): array - { - if ($hosts === null) { - return static::$hosts; - } - - if (is_int($hosts) && $hosts & static::HOST_FROM_SERVER) { - return static::$hosts = []; - } - - if (is_int($hosts) && $hosts & static::HOST_FROM_HEADER) { - return static::$hosts = ['*']; - } - - // make sure hosts are always an array - $hosts = A::wrap($hosts); - - // return unique hosts - return static::$hosts = array_unique($hosts); - } - - /** - * Checks for a https request - * - * @return bool - */ - public static function https(): bool - { - $https = $_SERVER['HTTPS'] ?? null; - $off = ['off', null, '', 0, '0', false, 'false', -1, '-1']; - - // check for various options to send a negative HTTPS header - if (in_array($https, $off, true) === false) { - return true; - } - - // check for the port - if (static::port() === 443) { - return true; - } - - return false; - } - - /** - * Checks for allowed host names - * - * @param string $host - * @return bool - */ - public static function isAllowedHost(string $host): bool - { - if (empty(static::$hosts) === true) { - return true; - } - - foreach (static::$hosts as $pattern) { - if (empty($pattern) === true) { - continue; - } - - if (fnmatch($pattern, $host) === true) { - return true; - } - } - - return false; - } - - /** - * Checks if the server is behind a - * proxy server. - * - * @return bool - */ - public static function isBehindProxy(): bool - { - return empty($_SERVER['HTTP_X_FORWARDED_HOST']) === false; - } - - /** - * Returns the correct port number - * - * @param bool $forwarded Deprecated. Todo: remove in 3.7.0 - * @return int - */ - public static function port(bool $forwarded = false): int - { - $port = null; - - // handle reverse proxy setups - if (static::isBehindProxy() === true) { - // based on forwarded port - $port = static::get('HTTP_X_FORWARDED_PORT'); - - // based on the forwarded host - if (empty($port) === true) { - $port = (int)parse_url(static::get('HTTP_X_FORWARDED_HOST'), PHP_URL_PORT); - } - - // based on the forwarded proto - if (empty($port) === true) { - if (in_array($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? null, ['https', 'https, http']) === true) { - $port = 443; - } - } - } - - // based on the host - if (empty($port) === true) { - $port = (int)parse_url(static::get('HTTP_HOST'), PHP_URL_PORT); - } - - // based on server port - if (empty($port) === true) { - $port = static::get('SERVER_PORT'); - } - - return $port ?? 0; - } - - /** - * Returns an array with path and query - * from the REQUEST_URI - * - * @return array - */ - public static function requestUri(): array - { - $uri = static::get('REQUEST_URI', ''); - $uri = parse_url($uri); - - return [ - 'path' => $uri['path'] ?? null, - 'query' => $uri['query'] ?? null, - ]; - } - - /** - * Help to sanitize some _SERVER keys - * - * @param string $key - * @param mixed $value - * @return mixed - */ - public static function sanitize(string $key, $value) - { - switch ($key) { - case 'SERVER_ADDR': - case 'SERVER_NAME': - case 'HTTP_HOST': - case 'HTTP_X_FORWARDED_HOST': - $value ??= ''; - $value = strtolower($value); - $value = strip_tags($value); - $value = basename($value); - $value = preg_replace('![^\w.:-]+!iu', '', $value); - $value = htmlspecialchars($value, ENT_COMPAT); - $value = trim($value, '-'); - $value = trim($value, '.'); - break; - case 'SERVER_PORT': - case 'HTTP_X_FORWARDED_PORT': - $value ??= ''; - $value = (int)(preg_replace('![^0-9]+!', '', $value)); - break; - } - - return $value; - } - - /** - * Returns the path to the php script - * within the document root without the - * filename of the script. - * - * i.e. /subfolder/index.php -> subfolder - * - * This can be used to build the base url - * for subfolder installations - * - * @return string - */ - public static function scriptPath(): string - { - if (static::cli() === true) { - return ''; - } - - $path = $_SERVER['SCRIPT_NAME'] ?? ''; - // replace Windows backslashes - $path = str_replace('\\', '/', $path); - // remove the script - $path = dirname($path); - // replace those fucking backslashes again - $path = str_replace('\\', '/', $path); - // remove the leading and trailing slashes - $path = trim($path, '/'); - - // top-level scripts don't have a path - // and dirname() will return '.' - if ($path === '.') { - $path = ''; - } - - return $path; - } + /** + * @return \Kirby\Http\Environment + */ + public static function instance() + { + return new Environment([ + 'cli' => static::$cli, + 'allowed' => static::$hosts + ]); + } } diff --git a/kirby/src/Http/Uri.php b/kirby/src/Http/Uri.php index 54b046f..48f8587 100644 --- a/kirby/src/Http/Uri.php +++ b/kirby/src/Http/Uri.php @@ -2,6 +2,7 @@ namespace Kirby\Http; +use Kirby\Cms\App; use Kirby\Exception\InvalidArgumentException; use Kirby\Toolkit\Properties; use Throwable; @@ -17,526 +18,570 @@ use Throwable; */ class Uri { - use Properties; - - /** - * Cache for the current Uri object - * - * @var Uri|null - */ - public static $current; - - /** - * The fragment after the hash - * - * @var string|false - */ - protected $fragment; - - /** - * The host address - * - * @var string - */ - protected $host; - - /** - * The optional password for basic authentication - * - * @var string|false - */ - protected $password; - - /** - * The optional list of params - * - * @var Params - */ - protected $params; - - /** - * The optional path - * - * @var Path - */ - protected $path; - - /** - * The optional port number - * - * @var int|false - */ - protected $port; - - /** - * All original properties - * - * @var array - */ - protected $props; - - /** - * The optional query string without leading ? - * - * @var Query - */ - protected $query; - - /** - * https or http - * - * @var string - */ - protected $scheme = 'http'; - - /** - * @var bool - */ - protected $slash = false; - - /** - * The optional username for basic authentication - * - * @var string|false - */ - protected $username; - - /** - * Magic caller to access all properties - * - * @param string $property - * @param array $arguments - * @return mixed - */ - public function __call(string $property, array $arguments = []) - { - return $this->$property ?? null; - } - - /** - * Make sure that cloning also clones - * the path and query objects - * - * @return void - */ - public function __clone() - { - $this->path = clone $this->path; - $this->query = clone $this->query; - $this->params = clone $this->params; - } - - /** - * Creates a new URI object - * - * @param array|string $props - * @param array $inject - */ - public function __construct($props = [], array $inject = []) - { - if (is_string($props) === true) { - $props = parse_url($props); - $props['username'] = $props['user'] ?? null; - $props['password'] = $props['pass'] ?? null; - - $props = array_merge($props, $inject); - } - - // parse the path and extract params - if (empty($props['path']) === false) { - $extract = Params::extract($props['path']); - $props['params'] ??= $extract['params']; - $props['path'] = $extract['path']; - $props['slash'] ??= $extract['slash']; - } - - $this->setProperties($this->props = $props); - } - - /** - * Magic getter - * - * @param string $property - * @return mixed - */ - public function __get(string $property) - { - return $this->$property ?? null; - } - - /** - * Magic setter - * - * @param string $property - * @param mixed $value - */ - public function __set(string $property, $value) - { - if (method_exists($this, 'set' . $property) === true) { - $this->{'set' . $property}($value); - } - } - - /** - * Converts the URL object to string - * - * @return string - */ - public function __toString(): string - { - try { - return $this->toString(); - } catch (Throwable $e) { - return ''; - } - } - - /** - * Returns the auth details (username:password) - * - * @return string|null - */ - public function auth(): ?string - { - $auth = trim($this->username . ':' . $this->password); - return $auth !== ':' ? $auth : null; - } - - /** - * Returns the base url (scheme + host) - * without trailing slash - * - * @return string|null - */ - public function base(): ?string - { - if ($domain = $this->domain()) { - return $this->scheme ? $this->scheme . '://' . $domain : $domain; - } - - return null; - } - - /** - * Clones the Uri object and applies optional - * new props. - * - * @param array $props - * @return static - */ - public function clone(array $props = []) - { - $clone = clone $this; - - foreach ($props as $key => $value) { - $clone->__set($key, $value); - } - - return $clone; - } - - /** - * @param array $props - * @param bool $forwarded Deprecated! Todo: remove in 3.7.0 - * @return static - */ - public static function current(array $props = [], bool $forwarded = false) - { - if (static::$current !== null) { - return static::$current; - } - - $uri = Server::requestUri(); - $url = new static(array_merge([ - 'scheme' => Server::https() === true ? 'https' : 'http', - 'host' => Server::host(), - 'port' => Server::port(), - 'path' => $uri['path'], - 'query' => $uri['query'], - ], $props)); - - return $url; - } - - /** - * Returns the domain without scheme, path or query - * - * @return string|null - */ - public function domain(): ?string - { - if (empty($this->host) === true || $this->host === '/') { - return null; - } - - $auth = $this->auth(); - $domain = ''; - - if ($auth !== null) { - $domain .= $auth . '@'; - } - - $domain .= $this->host; - - if ($this->port !== null && in_array($this->port, [80, 443]) === false) { - $domain .= ':' . $this->port; - } - - return $domain; - } - - /** - * @return bool - */ - public function hasFragment(): bool - { - return empty($this->fragment) === false; - } - - /** - * @return bool - */ - public function hasPath(): bool - { - return $this->path()->isNotEmpty(); - } - - /** - * @return bool - */ - public function hasQuery(): bool - { - return $this->query()->isNotEmpty(); - } - - /** - * Tries to convert the internationalized host - * name to the human-readable UTF8 representation - * - * @return $this - */ - public function idn() - { - if (empty($this->host) === false) { - $this->setHost(Idn::decode($this->host)); - } - return $this; - } - - /** - * Creates an Uri object for the URL to the index.php - * or any other executed script. - * - * @param array $props - * @param bool $forwarded Deprecated! Todo: remove in 3.7.0 - * @return string - */ - public static function index(array $props = [], bool $forwarded = false) - { - return static::current(array_merge($props, [ - 'path' => Server::scriptPath(), - 'query' => null, - 'fragment' => null, - ])); - } - - - /** - * Checks if the host exists - * - * @return bool - */ - public function isAbsolute(): bool - { - return empty($this->host) === false; - } - - /** - * @param string|null $fragment - * @return $this - */ - public function setFragment(string $fragment = null) - { - $this->fragment = $fragment ? ltrim($fragment, '#') : null; - return $this; - } - - /** - * @param string $host - * @return $this - */ - public function setHost(string $host = null) - { - $this->host = $host; - return $this; - } - - /** - * @param \Kirby\Http\Params|string|array|null $params - * @return $this - */ - public function setParams($params = null) - { - $this->params = is_a($params, 'Kirby\Http\Params') === true ? $params : new Params($params); - return $this; - } - - /** - * @param string|null $password - * @return $this - */ - public function setPassword(string $password = null) - { - $this->password = $password; - return $this; - } - - /** - * @param \Kirby\Http\Path|string|array|null $path - * @return $this - */ - public function setPath($path = null) - { - $this->path = is_a($path, 'Kirby\Http\Path') === true ? $path : new Path($path); - return $this; - } - - /** - * @param int|null $port - * @return $this - */ - public function setPort(int $port = null) - { - if ($port === 0) { - $port = null; - } - - if ($port !== null) { - if ($port < 1 || $port > 65535) { - throw new InvalidArgumentException('Invalid port format: ' . $port); - } - } - - $this->port = $port; - return $this; - } - - /** - * @param \Kirby\Http\Query|string|array|null $query - * @return $this - */ - public function setQuery($query = null) - { - $this->query = is_a($query, 'Kirby\Http\Query') === true ? $query : new Query($query); - return $this; - } - - /** - * @param string $scheme - * @return $this - */ - public function setScheme(string $scheme = null) - { - if ($scheme !== null && in_array($scheme, ['http', 'https', 'ftp']) === false) { - throw new InvalidArgumentException('Invalid URL scheme: ' . $scheme); - } - - $this->scheme = $scheme; - return $this; - } - - /** - * Set if a trailing slash should be added to - * the path when the URI is being built - * - * @param bool $slash - * @return $this - */ - public function setSlash(bool $slash = false) - { - $this->slash = $slash; - return $this; - } - - /** - * @param string|null $username - * @return $this - */ - public function setUsername(string $username = null) - { - $this->username = $username; - return $this; - } - - /** - * Converts the Url object to an array - * - * @return array - */ - public function toArray(): array - { - $array = []; - - foreach ($this->propertyData as $key => $value) { - $value = $this->$key; - - if (is_object($value) === true) { - $value = $value->toArray(); - } - - $array[$key] = $value; - } - - return $array; - } - - public function toJson(...$arguments): string - { - return json_encode($this->toArray(), ...$arguments); - } - - /** - * Returns the full URL as string - * - * @return string - */ - public function toString(): string - { - $url = $this->base(); - $slash = true; - - if (empty($url) === true) { - $url = '/'; - $slash = false; - } - - $path = $this->path->toString($slash) . $this->params->toString(true); - - if ($this->slash && $slash === true) { - $path .= '/'; - } - - $url .= $path; - $url .= $this->query->toString(true); - - if (empty($this->fragment) === false) { - $url .= '#' . $this->fragment; - } - - return $url; - } - - /** - * Tries to convert a URL with an internationalized host - * name to the machine-readable Punycode representation - * - * @return $this - */ - public function unIdn() - { - if (empty($this->host) === false) { - $this->setHost(Idn::encode($this->host)); - } - return $this; - } + use Properties; + + /** + * Cache for the current Uri object + * + * @var Uri|null + */ + public static $current; + + /** + * The fragment after the hash + * + * @var string|false + */ + protected $fragment; + + /** + * The host address + * + * @var string + */ + protected $host; + + /** + * The optional password for basic authentication + * + * @var string|false + */ + protected $password; + + /** + * The optional list of params + * + * @var Params + */ + protected $params; + + /** + * The optional path + * + * @var Path + */ + protected $path; + + /** + * The optional port number + * + * @var int|false + */ + protected $port; + + /** + * All original properties + * + * @var array + */ + protected $props; + + /** + * The optional query string without leading ? + * + * @var Query + */ + protected $query; + + /** + * https or http + * + * @var string + */ + protected $scheme = 'http'; + + /** + * Supported schemes + * + * @var array + */ + protected static $schemes = ['http', 'https', 'ftp']; + + /** + * @var bool + */ + protected $slash = false; + + /** + * The optional username for basic authentication + * + * @var string|false + */ + protected $username; + + /** + * Magic caller to access all properties + * + * @param string $property + * @param array $arguments + * @return mixed + */ + public function __call(string $property, array $arguments = []) + { + return $this->$property ?? null; + } + + /** + * Make sure that cloning also clones + * the path and query objects + * + * @return void + */ + public function __clone() + { + $this->path = clone $this->path; + $this->query = clone $this->query; + $this->params = clone $this->params; + } + + /** + * Creates a new URI object + * + * @param array|string $props + * @param array $inject Additional props to inject if a URL string is passed + */ + public function __construct($props = [], array $inject = []) + { + if (is_string($props) === true) { + $props = parse_url($props); + $props['username'] = $props['user'] ?? null; + $props['password'] = $props['pass'] ?? null; + + $props = array_merge($props, $inject); + } + + // parse the path and extract params + if (empty($props['path']) === false) { + $props = static::parsePath($props); + } + + $this->setProperties($this->props = $props); + } + + /** + * Magic getter + * + * @param string $property + * @return mixed + */ + public function __get(string $property) + { + return $this->$property ?? null; + } + + /** + * Magic setter + * + * @param string $property + * @param mixed $value + * @return void + */ + public function __set(string $property, $value): void + { + if (method_exists($this, 'set' . $property) === true) { + $this->{'set' . $property}($value); + } + } + + /** + * Converts the URL object to string + * + * @return string + */ + public function __toString(): string + { + try { + return $this->toString(); + } catch (Throwable $e) { + return ''; + } + } + + /** + * Returns the auth details (username:password) + * + * @return string|null + */ + public function auth(): ?string + { + $auth = trim($this->username . ':' . $this->password); + return $auth !== ':' ? $auth : null; + } + + /** + * Returns the base url (scheme + host) + * without trailing slash + * + * @return string|null + */ + public function base(): ?string + { + if ($domain = $this->domain()) { + return $this->scheme ? $this->scheme . '://' . $domain : $domain; + } + + return null; + } + + /** + * Clones the Uri object and applies optional + * new props. + * + * @param array $props + * @return static + */ + public function clone(array $props = []) + { + $clone = clone $this; + + foreach ($props as $key => $value) { + $clone->__set($key, $value); + } + + return $clone; + } + + /** + * @param array $props + * @return static + */ + public static function current(array $props = []) + { + if (static::$current !== null) { + return static::$current; + } + + if ($app = App::instance(null, true)) { + $environment = $app->environment(); + } else { + $environment = new Environment(); + } + + return new static($environment->requestUrl(), $props); + } + + /** + * Returns the domain without scheme, path or query + * + * @return string|null + */ + public function domain(): ?string + { + if (empty($this->host) === true || $this->host === '/') { + return null; + } + + $auth = $this->auth(); + $domain = ''; + + if ($auth !== null) { + $domain .= $auth . '@'; + } + + $domain .= $this->host; + + if ($this->port !== null && in_array($this->port, [80, 443]) === false) { + $domain .= ':' . $this->port; + } + + return $domain; + } + + /** + * @return bool + */ + public function hasFragment(): bool + { + return empty($this->fragment) === false; + } + + /** + * @return bool + */ + public function hasPath(): bool + { + return $this->path()->isNotEmpty(); + } + + /** + * @return bool + */ + public function hasQuery(): bool + { + return $this->query()->isNotEmpty(); + } + + /** + * @return bool + */ + public function https(): bool + { + return $this->scheme() === 'https'; + } + + /** + * Tries to convert the internationalized host + * name to the human-readable UTF8 representation + * + * @return $this + */ + public function idn() + { + if (empty($this->host) === false) { + $this->setHost(Idn::decode($this->host)); + } + return $this; + } + + /** + * Creates an Uri object for the URL to the index.php + * or any other executed script. + * + * @param array $props + * @return static + */ + public static function index(array $props = []) + { + if ($app = App::instance(null, true)) { + $url = $app->url('index'); + } else { + $url = (new Environment())->baseUrl(); + } + + return new static($url, $props); + } + + /** + * Checks if the host exists + * + * @return bool + */ + public function isAbsolute(): bool + { + return empty($this->host) === false; + } + + /** + * @param string|null $fragment + * @return $this + */ + public function setFragment(string $fragment = null) + { + $this->fragment = $fragment ? ltrim($fragment, '#') : null; + return $this; + } + + /** + * @param string $host + * @return $this + */ + public function setHost(string $host = null) + { + $this->host = $host; + return $this; + } + + /** + * @param \Kirby\Http\Params|string|array|false|null $params + * @return $this + */ + public function setParams($params = null) + { + // ensure that the special constructor value of `false` + // is never passed through as it's not supported by `Params` + if ($params === false) { + $params = []; + } + + $this->params = is_a($params, 'Kirby\Http\Params') === true ? $params : new Params($params); + return $this; + } + + /** + * @param string|null $password + * @return $this + */ + public function setPassword(string $password = null) + { + $this->password = $password; + return $this; + } + + /** + * @param \Kirby\Http\Path|string|array|null $path + * @return $this + */ + public function setPath($path = null) + { + $this->path = is_a($path, 'Kirby\Http\Path') === true ? $path : new Path($path); + return $this; + } + + /** + * @param int|null $port + * @return $this + */ + public function setPort(int $port = null) + { + if ($port === 0) { + $port = null; + } + + if ($port !== null) { + if ($port < 1 || $port > 65535) { + throw new InvalidArgumentException('Invalid port format: ' . $port); + } + } + + $this->port = $port; + return $this; + } + + /** + * @param \Kirby\Http\Query|string|array|null $query + * @return $this + */ + public function setQuery($query = null) + { + $this->query = is_a($query, 'Kirby\Http\Query') === true ? $query : new Query($query); + return $this; + } + + /** + * @param string $scheme + * @return $this + */ + public function setScheme(string $scheme = null) + { + if ($scheme !== null && in_array($scheme, static::$schemes) === false) { + throw new InvalidArgumentException('Invalid URL scheme: ' . $scheme); + } + + $this->scheme = $scheme; + return $this; + } + + /** + * Set if a trailing slash should be added to + * the path when the URI is being built + * + * @param bool $slash + * @return $this + */ + public function setSlash(bool $slash = false) + { + $this->slash = $slash; + return $this; + } + + /** + * @param string|null $username + * @return $this + */ + public function setUsername(string $username = null) + { + $this->username = $username; + return $this; + } + + /** + * Converts the Url object to an array + * + * @return array + */ + public function toArray(): array + { + $array = []; + + foreach ($this->propertyData as $key => $value) { + $value = $this->$key; + + if (is_object($value) === true) { + $value = $value->toArray(); + } + + $array[$key] = $value; + } + + return $array; + } + + public function toJson(...$arguments): string + { + return json_encode($this->toArray(), ...$arguments); + } + + /** + * Returns the full URL as string + * + * @return string + */ + public function toString(): string + { + $url = $this->base(); + $slash = true; + + if (empty($url) === true) { + $url = '/'; + $slash = false; + } + + $path = $this->path->toString($slash) . $this->params->toString(true); + + if ($this->slash && $slash === true) { + $path .= '/'; + } + + $url .= $path; + $url .= $this->query->toString(true); + + if (empty($this->fragment) === false) { + $url .= '#' . $this->fragment; + } + + return $url; + } + + /** + * Tries to convert a URL with an internationalized host + * name to the machine-readable Punycode representation + * + * @return $this + */ + public function unIdn() + { + if (empty($this->host) === false) { + $this->setHost(Idn::encode($this->host)); + } + return $this; + } + + /** + * Parses the path inside the props and extracts + * the params unless disabled + * + * @param array $props + * @return array Modified props array + */ + protected static function parsePath(array $props): array + { + // extract params, the rest is the path; + // only do this if not explicitly disabled (set to `false`) + if (isset($props['params']) === false || $props['params'] !== false) { + $extract = Params::extract($props['path']); + $props['params'] ??= $extract['params']; + $props['path'] = $extract['path']; + $props['slash'] ??= $extract['slash']; + + return $props; + } + + // use the full path; + // automatically detect the trailing slash from it if possible + if (is_string($props['path']) === true) { + $props['slash'] = substr($props['path'], -1, 1) === '/'; + } + + return $props; + } } diff --git a/kirby/src/Http/Url.php b/kirby/src/Http/Url.php index d9d23c2..e0c3cdf 100644 --- a/kirby/src/Http/Url.php +++ b/kirby/src/Http/Url.php @@ -15,276 +15,275 @@ use Kirby\Toolkit\Str; */ class Url { - /** - * The base Url to build absolute Urls from - * - * @var string - */ - public static $home = '/'; + /** + * The base Url to build absolute Urls from + * + * @var string + */ + public static $home = '/'; - /** - * The current Uri object - * - * @var Uri - */ - public static $current = null; + /** + * The current Uri object + * + * @var Uri + */ + public static $current = null; - /** - * Facade for all Uri object methods - * - * @param string $method - * @param array $arguments - * @return mixed - */ - public static function __callStatic(string $method, $arguments) - { - return (new Uri($arguments[0] ?? static::current()))->$method(...array_slice($arguments, 1)); - } + /** + * Facade for all Uri object methods + * + * @param string $method + * @param array $arguments + * @return mixed + */ + public static function __callStatic(string $method, $arguments) + { + return (new Uri($arguments[0] ?? static::current()))->$method(...array_slice($arguments, 1)); + } - /** - * Url Builder - * Actually just a factory for `new Uri($parts)` - * - * @param array $parts - * @param string|null $url - * @return string - */ - public static function build(array $parts = [], string $url = null): string - { - return (string)(new Uri($url ?? static::current()))->clone($parts); - } + /** + * Url Builder + * Actually just a factory for `new Uri($parts)` + * + * @param array $parts + * @param string|null $url + * @return string + */ + public static function build(array $parts = [], string $url = null): string + { + return (string)(new Uri($url ?? static::current()))->clone($parts); + } - /** - * Returns the current url with all bells and whistles - * - * @return string - */ - public static function current(): string - { - return static::$current = static::$current ?? static::toObject()->toString(); - } + /** + * Returns the current url with all bells and whistles + * + * @return string + */ + public static function current(): string + { + return static::$current = static::$current ?? static::toObject()->toString(); + } - /** - * Returns the url for the current directory - * - * @return string - */ - public static function currentDir(): string - { - return dirname(static::current()); - } + /** + * Returns the url for the current directory + * + * @return string + */ + public static function currentDir(): string + { + return dirname(static::current()); + } - /** - * Tries to fix a broken url without protocol - * - * @param string|null $url - * @return string - */ - public static function fix(string $url = null): string - { - // make sure to not touch absolute urls - return (!preg_match('!^(https|http|ftp)\:\/\/!i', $url ?? '')) ? 'http://' . $url : $url; - } + /** + * Tries to fix a broken url without protocol + * + * @param string|null $url + * @return string + */ + public static function fix(string $url = null): string + { + // make sure to not touch absolute urls + return (!preg_match('!^(https|http|ftp)\:\/\/!i', $url ?? '')) ? 'http://' . $url : $url; + } - /** - * Returns the home url if defined - * - * @return string - */ - public static function home(): string - { - return static::$home; - } + /** + * Returns the home url if defined + * + * @return string + */ + public static function home(): string + { + return static::$home; + } - /** - * Returns the url to the executed script - * - * @param array $props - * @param bool $forwarded Deprecated! Todo: remove in 3.7.0 - * @return string - */ - public static function index(array $props = [], bool $forwarded = false): string - { - return Uri::index($props)->toString(); - } + /** + * Returns the url to the executed script + * + * @param array $props + * @return string + */ + public static function index(array $props = []): string + { + return Uri::index($props)->toString(); + } - /** - * Checks if an URL is absolute - * - * @param string|null $url - * @return bool - */ - public static function isAbsolute(string $url = null): bool - { - // matches the following groups of URLs: - // //example.com/uri - // http://example.com/uri, https://example.com/uri, ftp://example.com/uri - // mailto:example@example.com, geo:49.0158,8.3239?z=11 - return $url !== null && preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:|geo:)!i', $url) === 1; - } + /** + * Checks if an URL is absolute + * + * @param string|null $url + * @return bool + */ + public static function isAbsolute(string $url = null): bool + { + // matches the following groups of URLs: + // //example.com/uri + // http://example.com/uri, https://example.com/uri, ftp://example.com/uri + // mailto:example@example.com, geo:49.0158,8.3239?z=11 + return $url !== null && preg_match('!^(//|[a-z0-9+-.]+://|mailto:|tel:|geo:)!i', $url) === 1; + } - /** - * Convert a relative path into an absolute URL - * - * @param string|null $path - * @param string|null $home - * @return string - */ - public static function makeAbsolute(string $path = null, string $home = null): string - { - if ($path === '' || $path === '/' || $path === null) { - return $home ?? static::home(); - } + /** + * Convert a relative path into an absolute URL + * + * @param string|null $path + * @param string|null $home + * @return string + */ + public static function makeAbsolute(string $path = null, string $home = null): string + { + if ($path === '' || $path === '/' || $path === null) { + return $home ?? static::home(); + } - if (substr($path, 0, 1) === '#') { - return $path; - } + if (substr($path, 0, 1) === '#') { + return $path; + } - if (static::isAbsolute($path)) { - return $path; - } + if (static::isAbsolute($path)) { + return $path; + } - // build the full url - $path = ltrim($path, '/'); - $home ??= static::home(); + // build the full url + $path = ltrim($path, '/'); + $home ??= static::home(); - if (empty($path) === true) { - return $home; - } + if (empty($path) === true) { + return $home; + } - return $home === '/' ? '/' . $path : $home . '/' . $path; - } + return $home === '/' ? '/' . $path : $home . '/' . $path; + } - /** - * Returns the path for the given url - * - * @param string|array|null $url - * @param bool $leadingSlash - * @param bool $trailingSlash - * @return string - */ - public static function path($url = null, bool $leadingSlash = false, bool $trailingSlash = false): string - { - return Url::toObject($url)->path()->toString($leadingSlash, $trailingSlash); - } + /** + * Returns the path for the given url + * + * @param string|array|null $url + * @param bool $leadingSlash + * @param bool $trailingSlash + * @return string + */ + public static function path($url = null, bool $leadingSlash = false, bool $trailingSlash = false): string + { + return Url::toObject($url)->path()->toString($leadingSlash, $trailingSlash); + } - /** - * Returns the query for the given url - * - * @param string|array|null $url - * @return string - */ - public static function query($url = null): string - { - return Url::toObject($url)->query()->toString(); - } + /** + * Returns the query for the given url + * + * @param string|array|null $url + * @return string + */ + public static function query($url = null): string + { + return Url::toObject($url)->query()->toString(); + } - /** - * Return the last url the user has been on if detectable - * - * @return string - */ - public static function last(): string - { - return $_SERVER['HTTP_REFERER'] ?? ''; - } + /** + * Return the last url the user has been on if detectable + * + * @return string + */ + public static function last(): string + { + return Environment::getGlobally('HTTP_REFERER', ''); + } - /** - * Shortens the Url by removing all unnecessary parts - * - * @param string $url - * @param int $length - * @param bool $base - * @param string $rep - * @return string - */ - public static function short($url = null, int $length = 0, bool $base = false, string $rep = '…'): string - { - $uri = static::toObject($url); + /** + * Shortens the Url by removing all unnecessary parts + * + * @param string $url + * @param int $length + * @param bool $base + * @param string $rep + * @return string + */ + public static function short($url = null, int $length = 0, bool $base = false, string $rep = '…'): string + { + $uri = static::toObject($url); - $uri->fragment = null; - $uri->query = null; - $uri->password = null; - $uri->port = null; - $uri->scheme = null; - $uri->username = null; + $uri->fragment = null; + $uri->query = null; + $uri->password = null; + $uri->port = null; + $uri->scheme = null; + $uri->username = null; - // remove the trailing slash from the path - $uri->slash = false; + // remove the trailing slash from the path + $uri->slash = false; - $url = $base ? $uri->base() : $uri->toString(); - $url = str_replace('www.', '', $url); + $url = $base ? $uri->base() : $uri->toString(); + $url = str_replace('www.', '', $url); - return Str::short($url, $length, $rep); - } + return Str::short($url, $length, $rep); + } - /** - * Removes the path from the Url - * - * @param string $url - * @return string - */ - public static function stripPath($url = null): string - { - return static::toObject($url)->setPath(null)->toString(); - } + /** + * Removes the path from the Url + * + * @param string $url + * @return string + */ + public static function stripPath($url = null): string + { + return static::toObject($url)->setPath(null)->toString(); + } - /** - * Removes the query string from the Url - * - * @param string $url - * @return string - */ - public static function stripQuery($url = null): string - { - return static::toObject($url)->setQuery(null)->toString(); - } + /** + * Removes the query string from the Url + * + * @param string $url + * @return string + */ + public static function stripQuery($url = null): string + { + return static::toObject($url)->setQuery(null)->toString(); + } - /** - * Removes the fragment (hash) from the Url - * - * @param string $url - * @return string - */ - public static function stripFragment($url = null): string - { - return static::toObject($url)->setFragment(null)->toString(); - } + /** + * Removes the fragment (hash) from the Url + * + * @param string $url + * @return string + */ + public static function stripFragment($url = null): string + { + return static::toObject($url)->setFragment(null)->toString(); + } - /** - * Smart resolver for internal and external urls - * - * @param string $path - * @param mixed $options - * @return string - */ - public static function to(string $path = null, $options = null): string - { - // make sure $path is string - $path ??= ''; + /** + * Smart resolver for internal and external urls + * + * @param string $path + * @param mixed $options + * @return string + */ + public static function to(string $path = null, $options = null): string + { + // make sure $path is string + $path ??= ''; - // keep relative urls - if (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') { - return $path; - } + // keep relative urls + if (substr($path, 0, 2) === './' || substr($path, 0, 3) === '../') { + return $path; + } - $url = static::makeAbsolute($path); + $url = static::makeAbsolute($path); - if ($options === null) { - return $url; - } + if ($options === null) { + return $url; + } - return (new Uri($url, $options))->toString(); - } + return (new Uri($url, $options))->toString(); + } - /** - * Converts the Url to a Uri object - * - * @param string $url - * @return \Kirby\Http\Uri - */ - public static function toObject($url = null) - { - return $url === null ? Uri::current() : new Uri($url); - } + /** + * Converts the Url to a Uri object + * + * @param string $url + * @return \Kirby\Http\Uri + */ + public static function toObject($url = null) + { + return $url === null ? Uri::current() : new Uri($url); + } } diff --git a/kirby/src/Http/Visitor.php b/kirby/src/Http/Visitor.php index 97907cc..6c1b2ac 100644 --- a/kirby/src/Http/Visitor.php +++ b/kirby/src/Http/Visitor.php @@ -20,233 +20,233 @@ use Kirby\Toolkit\Str; */ class Visitor { - /** - * IP address - * @var string|null - */ - protected $ip; + /** + * IP address + * @var string|null + */ + protected $ip; - /** - * user agent - * @var string|null - */ - protected $userAgent; + /** + * user agent + * @var string|null + */ + protected $userAgent; - /** - * accepted language - * @var string|null - */ - protected $acceptedLanguage; + /** + * accepted language + * @var string|null + */ + protected $acceptedLanguage; - /** - * accepted mime type - * @var string|null - */ - protected $acceptedMimeType; + /** + * accepted mime type + * @var string|null + */ + protected $acceptedMimeType; - /** - * Creates a new visitor object. - * Optional arguments can be passed to - * modify the information about the visitor. - * - * By default everything is pulled from $_SERVER - * - * @param array $arguments - */ - public function __construct(array $arguments = []) - { - $this->ip($arguments['ip'] ?? $_SERVER['REMOTE_ADDR'] ?? ''); - $this->userAgent($arguments['userAgent'] ?? $_SERVER['HTTP_USER_AGENT'] ?? ''); - $this->acceptedLanguage($arguments['acceptedLanguage'] ?? $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''); - $this->acceptedMimeType($arguments['acceptedMimeType'] ?? $_SERVER['HTTP_ACCEPT'] ?? ''); - } + /** + * Creates a new visitor object. + * Optional arguments can be passed to + * modify the information about the visitor. + * + * By default everything is pulled from $_SERVER + * + * @param array $arguments + */ + public function __construct(array $arguments = []) + { + $this->ip($arguments['ip'] ?? Environment::getGlobally('REMOTE_ADDR', '')); + $this->userAgent($arguments['userAgent'] ?? Environment::getGlobally('HTTP_USER_AGENT', '')); + $this->acceptedLanguage($arguments['acceptedLanguage'] ?? Environment::getGlobally('HTTP_ACCEPT_LANGUAGE', '')); + $this->acceptedMimeType($arguments['acceptedMimeType'] ?? Environment::getGlobally('HTTP_ACCEPT', '')); + } - /** - * Sets the accepted language if - * provided or returns the user's - * accepted language otherwise - * - * @param string|null $acceptedLanguage - * @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor|null - */ - public function acceptedLanguage(string $acceptedLanguage = null) - { - if ($acceptedLanguage === null) { - return $this->acceptedLanguages()->first(); - } + /** + * Sets the accepted language if + * provided or returns the user's + * accepted language otherwise + * + * @param string|null $acceptedLanguage + * @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor|null + */ + public function acceptedLanguage(string $acceptedLanguage = null) + { + if ($acceptedLanguage === null) { + return $this->acceptedLanguages()->first(); + } - $this->acceptedLanguage = $acceptedLanguage; - return $this; - } + $this->acceptedLanguage = $acceptedLanguage; + return $this; + } - /** - * Returns an array of all accepted languages - * including their quality and locale - * - * @return \Kirby\Toolkit\Collection - */ - public function acceptedLanguages() - { - $accepted = Str::accepted($this->acceptedLanguage); - $languages = []; + /** + * Returns an array of all accepted languages + * including their quality and locale + * + * @return \Kirby\Toolkit\Collection + */ + public function acceptedLanguages() + { + $accepted = Str::accepted($this->acceptedLanguage); + $languages = []; - foreach ($accepted as $language) { - $value = $language['value']; - $parts = Str::split($value, '-'); - $code = isset($parts[0]) ? Str::lower($parts[0]) : null; - $region = isset($parts[1]) ? Str::upper($parts[1]) : null; - $locale = $region ? $code . '_' . $region : $code; + foreach ($accepted as $language) { + $value = $language['value']; + $parts = Str::split($value, '-'); + $code = isset($parts[0]) ? Str::lower($parts[0]) : null; + $region = isset($parts[1]) ? Str::upper($parts[1]) : null; + $locale = $region ? $code . '_' . $region : $code; - $languages[$locale] = new Obj([ - 'code' => $code, - 'locale' => $locale, - 'original' => $value, - 'quality' => $language['quality'], - 'region' => $region, - ]); - } + $languages[$locale] = new Obj([ + 'code' => $code, + 'locale' => $locale, + 'original' => $value, + 'quality' => $language['quality'], + 'region' => $region, + ]); + } - return new Collection($languages); - } + return new Collection($languages); + } - /** - * Checks if the user accepts the given language - * - * @param string $code - * @return bool - */ - public function acceptsLanguage(string $code): bool - { - $mode = Str::contains($code, '_') === true ? 'locale' : 'code'; + /** + * Checks if the user accepts the given language + * + * @param string $code + * @return bool + */ + public function acceptsLanguage(string $code): bool + { + $mode = Str::contains($code, '_') === true ? 'locale' : 'code'; - foreach ($this->acceptedLanguages() as $language) { - if ($language->$mode() === $code) { - return true; - } - } + foreach ($this->acceptedLanguages() as $language) { + if ($language->$mode() === $code) { + return true; + } + } - return false; - } + return false; + } - /** - * Sets the accepted mime type if - * provided or returns the user's - * accepted mime type otherwise - * - * @param string|null $acceptedMimeType - * @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor - */ - public function acceptedMimeType(string $acceptedMimeType = null) - { - if ($acceptedMimeType === null) { - return $this->acceptedMimeTypes()->first(); - } + /** + * Sets the accepted mime type if + * provided or returns the user's + * accepted mime type otherwise + * + * @param string|null $acceptedMimeType + * @return \Kirby\Toolkit\Obj|\Kirby\Http\Visitor + */ + public function acceptedMimeType(string $acceptedMimeType = null) + { + if ($acceptedMimeType === null) { + return $this->acceptedMimeTypes()->first(); + } - $this->acceptedMimeType = $acceptedMimeType; - return $this; - } + $this->acceptedMimeType = $acceptedMimeType; + return $this; + } - /** - * Returns a collection of all accepted mime types - * - * @return \Kirby\Toolkit\Collection - */ - public function acceptedMimeTypes() - { - $accepted = Str::accepted($this->acceptedMimeType); - $mimes = []; + /** + * Returns a collection of all accepted mime types + * + * @return \Kirby\Toolkit\Collection + */ + public function acceptedMimeTypes() + { + $accepted = Str::accepted($this->acceptedMimeType); + $mimes = []; - foreach ($accepted as $mime) { - $mimes[$mime['value']] = new Obj([ - 'type' => $mime['value'], - 'quality' => $mime['quality'], - ]); - } + foreach ($accepted as $mime) { + $mimes[$mime['value']] = new Obj([ + 'type' => $mime['value'], + 'quality' => $mime['quality'], + ]); + } - return new Collection($mimes); - } + return new Collection($mimes); + } - /** - * Checks if the user accepts the given mime type - * - * @param string $mimeType - * @return bool - */ - public function acceptsMimeType(string $mimeType): bool - { - return Mime::isAccepted($mimeType, $this->acceptedMimeType); - } + /** + * Checks if the user accepts the given mime type + * + * @param string $mimeType + * @return bool + */ + public function acceptsMimeType(string $mimeType): bool + { + return Mime::isAccepted($mimeType, $this->acceptedMimeType); + } - /** - * Returns the MIME type from the provided list that - * is most accepted (= preferred) by the visitor - * @since 3.3.0 - * - * @param string ...$mimeTypes MIME types to query for - * @return string|null Preferred MIME type - */ - public function preferredMimeType(string ...$mimeTypes): ?string - { - foreach ($this->acceptedMimeTypes() as $acceptedMime) { - // look for direct matches - if (in_array($acceptedMime->type(), $mimeTypes)) { - return $acceptedMime->type(); - } + /** + * Returns the MIME type from the provided list that + * is most accepted (= preferred) by the visitor + * @since 3.3.0 + * + * @param string ...$mimeTypes MIME types to query for + * @return string|null Preferred MIME type + */ + public function preferredMimeType(string ...$mimeTypes): ?string + { + foreach ($this->acceptedMimeTypes() as $acceptedMime) { + // look for direct matches + if (in_array($acceptedMime->type(), $mimeTypes)) { + return $acceptedMime->type(); + } - // test each option against wildcard `Accept` values - foreach ($mimeTypes as $expectedMime) { - if (Mime::matches($expectedMime, $acceptedMime->type()) === true) { - return $expectedMime; - } - } - } + // test each option against wildcard `Accept` values + foreach ($mimeTypes as $expectedMime) { + if (Mime::matches($expectedMime, $acceptedMime->type()) === true) { + return $expectedMime; + } + } + } - return null; - } + return null; + } - /** - * Returns true if the visitor prefers a JSON response over - * an HTML response based on the `Accept` request header - * @since 3.3.0 - * - * @return bool - */ - public function prefersJson(): bool - { - return $this->preferredMimeType('application/json', 'text/html') === 'application/json'; - } + /** + * Returns true if the visitor prefers a JSON response over + * an HTML response based on the `Accept` request header + * @since 3.3.0 + * + * @return bool + */ + public function prefersJson(): bool + { + return $this->preferredMimeType('application/json', 'text/html') === 'application/json'; + } - /** - * Sets the ip address if provided - * or returns the ip of the current - * visitor otherwise - * - * @param string|null $ip - * @return string|Visitor|null - */ - public function ip(string $ip = null) - { - if ($ip === null) { - return $this->ip; - } - $this->ip = $ip; - return $this; - } + /** + * Sets the ip address if provided + * or returns the ip of the current + * visitor otherwise + * + * @param string|null $ip + * @return string|Visitor|null + */ + public function ip(string $ip = null) + { + if ($ip === null) { + return $this->ip; + } + $this->ip = $ip; + return $this; + } - /** - * Sets the user agent if provided - * or returns the user agent string of - * the current visitor otherwise - * - * @param string|null $userAgent - * @return string|Visitor|null - */ - public function userAgent(string $userAgent = null) - { - if ($userAgent === null) { - return $this->userAgent; - } - $this->userAgent = $userAgent; - return $this; - } + /** + * Sets the user agent if provided + * or returns the user agent string of + * the current visitor otherwise + * + * @param string|null $userAgent + * @return string|Visitor|null + */ + public function userAgent(string $userAgent = null) + { + if ($userAgent === null) { + return $this->userAgent; + } + $this->userAgent = $userAgent; + return $this; + } } diff --git a/kirby/src/Image/Camera.php b/kirby/src/Image/Camera.php index 135bd3c..3cfb793 100644 --- a/kirby/src/Image/Camera.php +++ b/kirby/src/Image/Camera.php @@ -13,81 +13,81 @@ namespace Kirby\Image; */ class Camera { - /** - * Make exif data - * - * @var string|null - */ - protected $make; + /** + * Make exif data + * + * @var string|null + */ + protected $make; - /** - * Model exif data - * - * @var string|null - */ - protected $model; + /** + * Model exif data + * + * @var string|null + */ + protected $model; - /** - * Constructor - * - * @param array $exif - */ - public function __construct(array $exif) - { - $this->make = $exif['Make'] ?? null; - $this->model = $exif['Model'] ?? null; - } + /** + * Constructor + * + * @param array $exif + */ + public function __construct(array $exif) + { + $this->make = $exif['Make'] ?? null; + $this->model = $exif['Model'] ?? null; + } - /** - * Returns the make of the camera - * - * @return string - */ - public function make(): ?string - { - return $this->make; - } + /** + * Returns the make of the camera + * + * @return string + */ + public function make(): ?string + { + return $this->make; + } - /** - * Returns the camera model - * - * @return string - */ - public function model(): ?string - { - return $this->model; - } + /** + * Returns the camera model + * + * @return string + */ + public function model(): ?string + { + return $this->model; + } - /** - * Converts the object into a nicely readable array - * - * @return array - */ - public function toArray(): array - { - return [ - 'make' => $this->make, - 'model' => $this->model - ]; - } + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray(): array + { + return [ + 'make' => $this->make, + 'model' => $this->model + ]; + } - /** - * Returns the full make + model name - * - * @return string - */ - public function __toString(): string - { - return trim($this->make . ' ' . $this->model); - } + /** + * Returns the full make + model name + * + * @return string + */ + public function __toString(): string + { + return trim($this->make . ' ' . $this->model); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } } diff --git a/kirby/src/Image/Darkroom.php b/kirby/src/Image/Darkroom.php index abcc41a..642273a 100644 --- a/kirby/src/Image/Darkroom.php +++ b/kirby/src/Image/Darkroom.php @@ -16,145 +16,145 @@ use Exception; */ class Darkroom { - public static $types = [ - 'gd' => 'Kirby\Image\Darkroom\GdLib', - 'im' => 'Kirby\Image\Darkroom\ImageMagick' - ]; + public static $types = [ + 'gd' => 'Kirby\Image\Darkroom\GdLib', + 'im' => 'Kirby\Image\Darkroom\ImageMagick' + ]; - /** - * @var array - */ - protected $settings = []; + /** + * @var array + */ + protected $settings = []; - /** - * Darkroom constructor - * - * @param array $settings - */ - public function __construct(array $settings = []) - { - $this->settings = array_merge($this->defaults(), $settings); - } + /** + * Darkroom constructor + * + * @param array $settings + */ + public function __construct(array $settings = []) + { + $this->settings = array_merge($this->defaults(), $settings); + } - /** - * Creates a new Darkroom instance for the given - * type/driver - * - * @param string $type - * @param array $settings - * @return mixed - * @throws \Exception - */ - public static function factory(string $type, array $settings = []) - { - if (isset(static::$types[$type]) === false) { - throw new Exception('Invalid Darkroom type'); - } + /** + * Creates a new Darkroom instance for the given + * type/driver + * + * @param string $type + * @param array $settings + * @return mixed + * @throws \Exception + */ + public static function factory(string $type, array $settings = []) + { + if (isset(static::$types[$type]) === false) { + throw new Exception('Invalid Darkroom type'); + } - $class = static::$types[$type]; - return new $class($settings); - } + $class = static::$types[$type]; + return new $class($settings); + } - /** - * Returns the default thumb settings - * - * @return array - */ - protected function defaults(): array - { - return [ - 'autoOrient' => true, - 'blur' => false, - 'crop' => false, - 'format' => null, - 'grayscale' => false, - 'height' => null, - 'quality' => 90, - 'scaleHeight' => null, - 'scaleWidth' => null, - 'width' => null, - ]; - } + /** + * Returns the default thumb settings + * + * @return array + */ + protected function defaults(): array + { + return [ + 'autoOrient' => true, + 'blur' => false, + 'crop' => false, + 'format' => null, + 'grayscale' => false, + 'height' => null, + 'quality' => 90, + 'scaleHeight' => null, + 'scaleWidth' => null, + 'width' => null, + ]; + } - /** - * Normalizes all thumb options - * - * @param array $options - * @return array - */ - protected function options(array $options = []): array - { - $options = array_merge($this->settings, $options); + /** + * Normalizes all thumb options + * + * @param array $options + * @return array + */ + protected function options(array $options = []): array + { + $options = array_merge($this->settings, $options); - // normalize the crop option - if ($options['crop'] === true) { - $options['crop'] = 'center'; - } + // normalize the crop option + if ($options['crop'] === true) { + $options['crop'] = 'center'; + } - // normalize the blur option - if ($options['blur'] === true) { - $options['blur'] = 10; - } + // normalize the blur option + if ($options['blur'] === true) { + $options['blur'] = 10; + } - // normalize the greyscale option - if (isset($options['greyscale']) === true) { - $options['grayscale'] = $options['greyscale']; - unset($options['greyscale']); - } + // normalize the greyscale option + if (isset($options['greyscale']) === true) { + $options['grayscale'] = $options['greyscale']; + unset($options['greyscale']); + } - // normalize the bw option - if (isset($options['bw']) === true) { - $options['grayscale'] = $options['bw']; - unset($options['bw']); - } + // normalize the bw option + if (isset($options['bw']) === true) { + $options['grayscale'] = $options['bw']; + unset($options['bw']); + } - if ($options['quality'] === null) { - $options['quality'] = $this->settings['quality']; - } + if ($options['quality'] === null) { + $options['quality'] = $this->settings['quality']; + } - return $options; - } + return $options; + } - /** - * Calculates the dimensions of the final thumb based - * on the given options and returns a full array with - * all the final options to be used for the image generator - * - * @param string $file - * @param array $options - * @return array - */ - public function preprocess(string $file, array $options = []) - { - $options = $this->options($options); - $image = new Image($file); + /** + * Calculates the dimensions of the final thumb based + * on the given options and returns a full array with + * all the final options to be used for the image generator + * + * @param string $file + * @param array $options + * @return array + */ + public function preprocess(string $file, array $options = []) + { + $options = $this->options($options); + $image = new Image($file); - $dimensions = $image->dimensions(); - $thumbDimensions = $dimensions->thumb($options); + $dimensions = $image->dimensions(); + $thumbDimensions = $dimensions->thumb($options); - $sourceWidth = $image->width(); - $sourceHeight = $image->height(); + $sourceWidth = $image->width(); + $sourceHeight = $image->height(); - $options['width'] = $thumbDimensions->width(); - $options['height'] = $thumbDimensions->height(); + $options['width'] = $thumbDimensions->width(); + $options['height'] = $thumbDimensions->height(); - // scale ratio compared to the source dimensions - $options['scaleWidth'] = $sourceWidth ? $options['width'] / $sourceWidth : null; - $options['scaleHeight'] = $sourceHeight ? $options['height'] / $sourceHeight : null; + // scale ratio compared to the source dimensions + $options['scaleWidth'] = $sourceWidth ? $options['width'] / $sourceWidth : null; + $options['scaleHeight'] = $sourceHeight ? $options['height'] / $sourceHeight : null; - return $options; - } + return $options; + } - /** - * This method must be replaced by the driver to run the - * actual image processing job. - * - * @param string $file - * @param array $options - * @return array - */ - public function process(string $file, array $options = []): array - { - return $this->preprocess($file, $options); - } + /** + * This method must be replaced by the driver to run the + * actual image processing job. + * + * @param string $file + * @param array $options + * @return array + */ + public function process(string $file, array $options = []): array + { + return $this->preprocess($file, $options); + } } diff --git a/kirby/src/Image/Darkroom/GdLib.php b/kirby/src/Image/Darkroom/GdLib.php index 971cbb7..bc381e6 100644 --- a/kirby/src/Image/Darkroom/GdLib.php +++ b/kirby/src/Image/Darkroom/GdLib.php @@ -17,108 +17,108 @@ use Kirby\Image\Darkroom; */ class GdLib extends Darkroom { - /** - * Processes the image with the SimpleImage library - * - * @param string $file - * @param array $options - * @return array - */ - public function process(string $file, array $options = []): array - { - $options = $this->preprocess($file, $options); - $mime = $this->mime($options); + /** + * Processes the image with the SimpleImage library + * + * @param string $file + * @param array $options + * @return array + */ + public function process(string $file, array $options = []): array + { + $options = $this->preprocess($file, $options); + $mime = $this->mime($options); - $image = new SimpleImage(); - $image->fromFile($file); + $image = new SimpleImage(); + $image->fromFile($file); - $image = $this->resize($image, $options); - $image = $this->autoOrient($image, $options); - $image = $this->blur($image, $options); - $image = $this->grayscale($image, $options); + $image = $this->resize($image, $options); + $image = $this->autoOrient($image, $options); + $image = $this->blur($image, $options); + $image = $this->grayscale($image, $options); - $image->toFile($file, $mime, $options['quality']); + $image->toFile($file, $mime, $options['quality']); - return $options; - } + return $options; + } - /** - * Activates the autoOrient option in SimpleImage - * unless this is deactivated - * - * @param \claviska\SimpleImage $image - * @param $options - * @return \claviska\SimpleImage - */ - protected function autoOrient(SimpleImage $image, $options) - { - if ($options['autoOrient'] === false) { - return $image; - } + /** + * Activates the autoOrient option in SimpleImage + * unless this is deactivated + * + * @param \claviska\SimpleImage $image + * @param $options + * @return \claviska\SimpleImage + */ + protected function autoOrient(SimpleImage $image, $options) + { + if ($options['autoOrient'] === false) { + return $image; + } - return $image->autoOrient(); - } + return $image->autoOrient(); + } - /** - * Wrapper around SimpleImage's resize and crop methods - * - * @param \claviska\SimpleImage $image - * @param array $options - * @return \claviska\SimpleImage - */ - protected function resize(SimpleImage $image, array $options) - { - if ($options['crop'] === false) { - return $image->resize($options['width'], $options['height']); - } + /** + * Wrapper around SimpleImage's resize and crop methods + * + * @param \claviska\SimpleImage $image + * @param array $options + * @return \claviska\SimpleImage + */ + protected function resize(SimpleImage $image, array $options) + { + if ($options['crop'] === false) { + return $image->resize($options['width'], $options['height']); + } - return $image->thumbnail($options['width'], $options['height'] ?? $options['width'], $options['crop']); - } + return $image->thumbnail($options['width'], $options['height'] ?? $options['width'], $options['crop']); + } - /** - * Applies the correct blur settings for SimpleImage - * - * @param \claviska\SimpleImage $image - * @param array $options - * @return \claviska\SimpleImage - */ - protected function blur(SimpleImage $image, array $options) - { - if ($options['blur'] === false) { - return $image; - } + /** + * Applies the correct blur settings for SimpleImage + * + * @param \claviska\SimpleImage $image + * @param array $options + * @return \claviska\SimpleImage + */ + protected function blur(SimpleImage $image, array $options) + { + if ($options['blur'] === false) { + return $image; + } - return $image->blur('gaussian', (int)$options['blur']); - } + return $image->blur('gaussian', (int)$options['blur']); + } - /** - * Applies grayscale conversion if activated in the options. - * - * @param \claviska\SimpleImage $image - * @param array $options - * @return \claviska\SimpleImage - */ - protected function grayscale(SimpleImage $image, array $options) - { - if ($options['grayscale'] === false) { - return $image; - } + /** + * Applies grayscale conversion if activated in the options. + * + * @param \claviska\SimpleImage $image + * @param array $options + * @return \claviska\SimpleImage + */ + protected function grayscale(SimpleImage $image, array $options) + { + if ($options['grayscale'] === false) { + return $image; + } - return $image->desaturate(); - } + return $image->desaturate(); + } - /** - * Returns mime type based on `format` option - * - * @param array $options - * @return string|null - */ - protected function mime(array $options): ?string - { - if ($options['format'] === null) { - return null; - } + /** + * Returns mime type based on `format` option + * + * @param array $options + * @return string|null + */ + protected function mime(array $options): ?string + { + if ($options['format'] === null) { + return null; + } - return Mime::fromExtension($options['format']); - } + return Mime::fromExtension($options['format']); + } } diff --git a/kirby/src/Image/Darkroom/ImageMagick.php b/kirby/src/Image/Darkroom/ImageMagick.php index 2e13cbc..33e33aa 100644 --- a/kirby/src/Image/Darkroom/ImageMagick.php +++ b/kirby/src/Image/Darkroom/ImageMagick.php @@ -17,238 +17,238 @@ use Kirby\Image\Darkroom; */ class ImageMagick extends Darkroom { - /** - * Activates imagemagick's auto-orient feature unless - * it is deactivated via the options - * - * @param string $file - * @param array $options - * @return string - */ - protected function autoOrient(string $file, array $options) - { - if ($options['autoOrient'] === true) { - return '-auto-orient'; - } - } + /** + * Activates imagemagick's auto-orient feature unless + * it is deactivated via the options + * + * @param string $file + * @param array $options + * @return string + */ + protected function autoOrient(string $file, array $options) + { + if ($options['autoOrient'] === true) { + return '-auto-orient'; + } + } - /** - * Applies the blur settings - * - * @param string $file - * @param array $options - * @return string - */ - protected function blur(string $file, array $options) - { - if ($options['blur'] !== false) { - return '-blur ' . escapeshellarg('0x' . $options['blur']); - } - } + /** + * Applies the blur settings + * + * @param string $file + * @param array $options + * @return string + */ + protected function blur(string $file, array $options) + { + if ($options['blur'] !== false) { + return '-blur ' . escapeshellarg('0x' . $options['blur']); + } + } - /** - * Keep animated gifs - * - * @param string $file - * @param array $options - * @return string - */ - protected function coalesce(string $file, array $options) - { - if (F::extension($file) === 'gif') { - return '-coalesce'; - } - } + /** + * Keep animated gifs + * + * @param string $file + * @param array $options + * @return string + */ + protected function coalesce(string $file, array $options) + { + if (F::extension($file) === 'gif') { + return '-coalesce'; + } + } - /** - * Creates the convert command with the right path to the binary file - * - * @param string $file - * @param array $options - * @return string - */ - protected function convert(string $file, array $options): string - { - $command = escapeshellarg($options['bin']); + /** + * Creates the convert command with the right path to the binary file + * + * @param string $file + * @param array $options + * @return string + */ + protected function convert(string $file, array $options): string + { + $command = escapeshellarg($options['bin']); - // limit to single-threading to keep CPU usage sane - $command .= ' -limit thread 1'; + // limit to single-threading to keep CPU usage sane + $command .= ' -limit thread 1'; - // add JPEG size hint to optimize CPU and memory usage - if (F::mime($file) === 'image/jpeg') { - // add hint only when downscaling - if ($options['scaleWidth'] < 1 && $options['scaleHeight'] < 1) { - $command .= ' -define ' . escapeshellarg(sprintf('jpeg:size=%dx%d', $options['width'], $options['height'])); - } - } + // add JPEG size hint to optimize CPU and memory usage + if (F::mime($file) === 'image/jpeg') { + // add hint only when downscaling + if ($options['scaleWidth'] < 1 && $options['scaleHeight'] < 1) { + $command .= ' -define ' . escapeshellarg(sprintf('jpeg:size=%dx%d', $options['width'], $options['height'])); + } + } - // append input file - return $command . ' ' . escapeshellarg($file); - } + // append input file + return $command . ' ' . escapeshellarg($file); + } - /** - * Returns additional default parameters for imagemagick - * - * @return array - */ - protected function defaults(): array - { - return parent::defaults() + [ - 'bin' => 'convert', - 'interlace' => false, - ]; - } + /** + * Returns additional default parameters for imagemagick + * + * @return array + */ + protected function defaults(): array + { + return parent::defaults() + [ + 'bin' => 'convert', + 'interlace' => false, + ]; + } - /** - * Applies the correct settings for grayscale images - * - * @param string $file - * @param array $options - * @return string - */ - protected function grayscale(string $file, array $options) - { - if ($options['grayscale'] === true) { - return '-colorspace gray'; - } - } + /** + * Applies the correct settings for grayscale images + * + * @param string $file + * @param array $options + * @return string + */ + protected function grayscale(string $file, array $options) + { + if ($options['grayscale'] === true) { + return '-colorspace gray'; + } + } - /** - * Applies the correct settings for interlaced JPEGs if - * activated via options - * - * @param string $file - * @param array $options - * @return string - */ - protected function interlace(string $file, array $options) - { - if ($options['interlace'] === true) { - return '-interlace line'; - } - } + /** + * Applies the correct settings for interlaced JPEGs if + * activated via options + * + * @param string $file + * @param array $options + * @return string + */ + protected function interlace(string $file, array $options) + { + if ($options['interlace'] === true) { + return '-interlace line'; + } + } - /** - * Creates and runs the full imagemagick command - * to process the image - * - * @param string $file - * @param array $options - * @return array - * @throws \Exception - */ - public function process(string $file, array $options = []): array - { - $options = $this->preprocess($file, $options); - $command = []; + /** + * Creates and runs the full imagemagick command + * to process the image + * + * @param string $file + * @param array $options + * @return array + * @throws \Exception + */ + public function process(string $file, array $options = []): array + { + $options = $this->preprocess($file, $options); + $command = []; - $command[] = $this->convert($file, $options); - $command[] = $this->strip($file, $options); - $command[] = $this->interlace($file, $options); - $command[] = $this->coalesce($file, $options); - $command[] = $this->grayscale($file, $options); - $command[] = $this->autoOrient($file, $options); - $command[] = $this->resize($file, $options); - $command[] = $this->quality($file, $options); - $command[] = $this->blur($file, $options); - $command[] = $this->save($file, $options); + $command[] = $this->convert($file, $options); + $command[] = $this->strip($file, $options); + $command[] = $this->interlace($file, $options); + $command[] = $this->coalesce($file, $options); + $command[] = $this->grayscale($file, $options); + $command[] = $this->autoOrient($file, $options); + $command[] = $this->resize($file, $options); + $command[] = $this->quality($file, $options); + $command[] = $this->blur($file, $options); + $command[] = $this->save($file, $options); - // remove all null values and join the parts - $command = implode(' ', array_filter($command)); + // remove all null values and join the parts + $command = implode(' ', array_filter($command)); - // try to execute the command - exec($command, $output, $return); + // try to execute the command + exec($command, $output, $return); - // log broken commands - if ($return !== 0) { - throw new Exception('The imagemagick convert command could not be executed: ' . $command); - } + // log broken commands + if ($return !== 0) { + throw new Exception('The imagemagick convert command could not be executed: ' . $command); + } - return $options; - } + return $options; + } - /** - * Applies the correct JPEG compression quality settings - * - * @param string $file - * @param array $options - * @return string - */ - protected function quality(string $file, array $options): string - { - return '-quality ' . escapeshellarg($options['quality']); - } + /** + * Applies the correct JPEG compression quality settings + * + * @param string $file + * @param array $options + * @return string + */ + protected function quality(string $file, array $options): string + { + return '-quality ' . escapeshellarg($options['quality']); + } - /** - * Creates the correct options to crop or resize the image - * and translates the crop positions for imagemagick - * - * @param string $file - * @param array $options - * @return string - */ - protected function resize(string $file, array $options): string - { - // simple resize - if ($options['crop'] === false) { - return '-thumbnail ' . escapeshellarg(sprintf('%sx%s!', $options['width'], $options['height'])); - } + /** + * Creates the correct options to crop or resize the image + * and translates the crop positions for imagemagick + * + * @param string $file + * @param array $options + * @return string + */ + protected function resize(string $file, array $options): string + { + // simple resize + if ($options['crop'] === false) { + return '-thumbnail ' . escapeshellarg(sprintf('%sx%s!', $options['width'], $options['height'])); + } - $gravities = [ - 'top left' => 'NorthWest', - 'top' => 'North', - 'top right' => 'NorthEast', - 'left' => 'West', - 'center' => 'Center', - 'right' => 'East', - 'bottom left' => 'SouthWest', - 'bottom' => 'South', - 'bottom right' => 'SouthEast' - ]; + $gravities = [ + 'top left' => 'NorthWest', + 'top' => 'North', + 'top right' => 'NorthEast', + 'left' => 'West', + 'center' => 'Center', + 'right' => 'East', + 'bottom left' => 'SouthWest', + 'bottom' => 'South', + 'bottom right' => 'SouthEast' + ]; - // translate the gravity option into something imagemagick understands - $gravity = $gravities[$options['crop']] ?? 'Center'; + // translate the gravity option into something imagemagick understands + $gravity = $gravities[$options['crop']] ?? 'Center'; - $command = '-thumbnail ' . escapeshellarg(sprintf('%sx%s^', $options['width'], $options['height'])); - $command .= ' -gravity ' . escapeshellarg($gravity); - $command .= ' -crop ' . escapeshellarg(sprintf('%sx%s+0+0', $options['width'], $options['height'])); + $command = '-thumbnail ' . escapeshellarg(sprintf('%sx%s^', $options['width'], $options['height'])); + $command .= ' -gravity ' . escapeshellarg($gravity); + $command .= ' -crop ' . escapeshellarg(sprintf('%sx%s+0+0', $options['width'], $options['height'])); - return $command; - } + return $command; + } - /** - * Creates the option for the output file - * - * @param string $file - * @param array $options - * @return string - */ - protected function save(string $file, array $options): string - { - if ($options['format'] !== null) { - $file = pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $options['format']; - } + /** + * Creates the option for the output file + * + * @param string $file + * @param array $options + * @return string + */ + protected function save(string $file, array $options): string + { + if ($options['format'] !== null) { + $file = pathinfo($file, PATHINFO_DIRNAME) . '/' . pathinfo($file, PATHINFO_FILENAME) . '.' . $options['format']; + } - return escapeshellarg($file); - } + return escapeshellarg($file); + } - /** - * Removes all metadata from the image - * - * @param string $file - * @param array $options - * @return string - */ - protected function strip(string $file, array $options): string - { - if (F::extension($file) === 'png') { - // ImageMagick does not support keeping ICC profiles while - // stripping other privacy- and security-related information, - // such as GPS data; so discard all color profiles for PNG files - // (tested with ImageMagick 7.0.11-14 Q16 x86_64 2021-05-31) - return '-strip'; - } + /** + * Removes all metadata from the image + * + * @param string $file + * @param array $options + * @return string + */ + protected function strip(string $file, array $options): string + { + if (F::extension($file) === 'png') { + // ImageMagick does not support keeping ICC profiles while + // stripping other privacy- and security-related information, + // such as GPS data; so discard all color profiles for PNG files + // (tested with ImageMagick 7.0.11-14 Q16 x86_64 2021-05-31) + return '-strip'; + } - return ''; - } + return ''; + } } diff --git a/kirby/src/Image/Dimensions.php b/kirby/src/Image/Dimensions.php index 23f9d40..30931c6 100644 --- a/kirby/src/Image/Dimensions.php +++ b/kirby/src/Image/Dimensions.php @@ -16,415 +16,415 @@ namespace Kirby\Image; */ class Dimensions { - /** - * the height of the parent object - * - * @var int - */ - public $height = 0; + /** + * the height of the parent object + * + * @var int + */ + public $height = 0; - /** - * the width of the parent object - * - * @var int - */ - public $width = 0; + /** + * the width of the parent object + * + * @var int + */ + public $width = 0; - /** - * Constructor - * - * @param int $width - * @param int $height - */ - public function __construct(int $width, int $height) - { - $this->width = $width; - $this->height = $height; - } + /** + * Constructor + * + * @param int $width + * @param int $height + */ + public function __construct(int $width, int $height) + { + $this->width = $width; + $this->height = $height; + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } - /** - * Echos the dimensions as width × height - * - * @return string - */ - public function __toString(): string - { - return $this->width . ' × ' . $this->height; - } + /** + * Echos the dimensions as width × height + * + * @return string + */ + public function __toString(): string + { + return $this->width . ' × ' . $this->height; + } - /** - * Crops the dimensions by width and height - * - * @param int $width - * @param int|null $height - * @return $this - */ - public function crop(int $width, int $height = null) - { - $this->width = $width; - $this->height = $width; + /** + * Crops the dimensions by width and height + * + * @param int $width + * @param int|null $height + * @return $this + */ + public function crop(int $width, int $height = null) + { + $this->width = $width; + $this->height = $width; - if ($height !== 0 && $height !== null) { - $this->height = $height; - } + if ($height !== 0 && $height !== null) { + $this->height = $height; + } - return $this; - } + return $this; + } - /** - * Returns the height - * - * @return int - */ - public function height() - { - return $this->height; - } + /** + * Returns the height + * + * @return int + */ + public function height() + { + return $this->height; + } - /** - * Recalculates the width and height to fit into the given box. - * - * - * - * $dimensions = new Dimensions(1200, 768); - * $dimensions->fit(500); - * - * echo $dimensions->width(); - * // output: 500 - * - * echo $dimensions->height(); - * // output: 320 - * - * - * - * @param int $box the max width and/or height - * @param bool $force If true, the dimensions will be - * upscaled to fit the box if smaller - * @return $this object with recalculated dimensions - */ - public function fit(int $box, bool $force = false) - { - if ($this->width === 0 || $this->height === 0) { - $this->width = $box; - $this->height = $box; - return $this; - } + /** + * Recalculates the width and height to fit into the given box. + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fit(500); + * + * echo $dimensions->width(); + * // output: 500 + * + * echo $dimensions->height(); + * // output: 320 + * + * + * + * @param int $box the max width and/or height + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return $this object with recalculated dimensions + */ + public function fit(int $box, bool $force = false) + { + if ($this->width === 0 || $this->height === 0) { + $this->width = $box; + $this->height = $box; + return $this; + } - $ratio = $this->ratio(); + $ratio = $this->ratio(); - if ($this->width > $this->height) { - // wider than tall - if ($this->width > $box || $force === true) { - $this->width = $box; - } - $this->height = (int)round($this->width / $ratio); - } elseif ($this->height > $this->width) { - // taller than wide - if ($this->height > $box || $force === true) { - $this->height = $box; - } - $this->width = (int)round($this->height * $ratio); - } elseif ($this->width > $box) { - // width = height but bigger than box - $this->width = $box; - $this->height = $box; - } + if ($this->width > $this->height) { + // wider than tall + if ($this->width > $box || $force === true) { + $this->width = $box; + } + $this->height = (int)round($this->width / $ratio); + } elseif ($this->height > $this->width) { + // taller than wide + if ($this->height > $box || $force === true) { + $this->height = $box; + } + $this->width = (int)round($this->height * $ratio); + } elseif ($this->width > $box) { + // width = height but bigger than box + $this->width = $box; + $this->height = $box; + } - return $this; - } + return $this; + } - /** - * Recalculates the width and height to fit the given height - * - * - * - * $dimensions = new Dimensions(1200, 768); - * $dimensions->fitHeight(500); - * - * echo $dimensions->width(); - * // output: 781 - * - * echo $dimensions->height(); - * // output: 500 - * - * - * - * @param int|null $fit the max height - * @param bool $force If true, the dimensions will be - * upscaled to fit the box if smaller - * @return $this object with recalculated dimensions - */ - public function fitHeight(int $fit = null, bool $force = false) - { - return $this->fitSize('height', $fit, $force); - } + /** + * Recalculates the width and height to fit the given height + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fitHeight(500); + * + * echo $dimensions->width(); + * // output: 781 + * + * echo $dimensions->height(); + * // output: 500 + * + * + * + * @param int|null $fit the max height + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return $this object with recalculated dimensions + */ + public function fitHeight(int $fit = null, bool $force = false) + { + return $this->fitSize('height', $fit, $force); + } - /** - * Helper for fitWidth and fitHeight methods - * - * @param string $ref reference (width or height) - * @param int|null $fit the max width - * @param bool $force If true, the dimensions will be - * upscaled to fit the box if smaller - * @return $this object with recalculated dimensions - */ - protected function fitSize(string $ref, int $fit = null, bool $force = false) - { - if ($fit === 0 || $fit === null) { - return $this; - } + /** + * Helper for fitWidth and fitHeight methods + * + * @param string $ref reference (width or height) + * @param int|null $fit the max width + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return $this object with recalculated dimensions + */ + protected function fitSize(string $ref, int $fit = null, bool $force = false) + { + if ($fit === 0 || $fit === null) { + return $this; + } - if ($this->$ref <= $fit && !$force) { - return $this; - } + if ($this->$ref <= $fit && !$force) { + return $this; + } - $ratio = $this->ratio(); - $mode = $ref === 'width'; - $this->width = $mode ? $fit : (int)round($fit * $ratio); - $this->height = !$mode ? $fit : (int)round($fit / $ratio); + $ratio = $this->ratio(); + $mode = $ref === 'width'; + $this->width = $mode ? $fit : (int)round($fit * $ratio); + $this->height = !$mode ? $fit : (int)round($fit / $ratio); - return $this; - } + return $this; + } - /** - * Recalculates the width and height to fit the given width - * - * - * - * $dimensions = new Dimensions(1200, 768); - * $dimensions->fitWidth(500); - * - * echo $dimensions->width(); - * // output: 500 - * - * echo $dimensions->height(); - * // output: 320 - * - * - * - * @param int|null $fit the max width - * @param bool $force If true, the dimensions will be - * upscaled to fit the box if smaller - * @return $this object with recalculated dimensions - */ - public function fitWidth(int $fit = null, bool $force = false) - { - return $this->fitSize('width', $fit, $force); - } + /** + * Recalculates the width and height to fit the given width + * + * + * + * $dimensions = new Dimensions(1200, 768); + * $dimensions->fitWidth(500); + * + * echo $dimensions->width(); + * // output: 500 + * + * echo $dimensions->height(); + * // output: 320 + * + * + * + * @param int|null $fit the max width + * @param bool $force If true, the dimensions will be + * upscaled to fit the box if smaller + * @return $this object with recalculated dimensions + */ + public function fitWidth(int $fit = null, bool $force = false) + { + return $this->fitSize('width', $fit, $force); + } - /** - * Recalculates the dimensions by the width and height - * - * @param int|null $width the max height - * @param int|null $height the max width - * @param bool $force - * @return $this - */ - public function fitWidthAndHeight(int $width = null, int $height = null, bool $force = false) - { - if ($this->width > $this->height) { - $this->fitWidth($width, $force); + /** + * Recalculates the dimensions by the width and height + * + * @param int|null $width the max height + * @param int|null $height the max width + * @param bool $force + * @return $this + */ + public function fitWidthAndHeight(int $width = null, int $height = null, bool $force = false) + { + if ($this->width > $this->height) { + $this->fitWidth($width, $force); - // do another check for the max height - if ($this->height > $height) { - $this->fitHeight($height); - } - } else { - $this->fitHeight($height, $force); + // do another check for the max height + if ($this->height > $height) { + $this->fitHeight($height); + } + } else { + $this->fitHeight($height, $force); - // do another check for the max width - if ($this->width > $width) { - $this->fitWidth($width); - } - } + // do another check for the max width + if ($this->width > $width) { + $this->fitWidth($width); + } + } - return $this; - } + return $this; + } - /** - * Detect the dimensions for an image file - * - * @param string $root - * @return static - */ - public static function forImage(string $root) - { - if (file_exists($root) === false) { - return new static(0, 0); - } + /** + * Detect the dimensions for an image file + * + * @param string $root + * @return static + */ + public static function forImage(string $root) + { + if (file_exists($root) === false) { + return new static(0, 0); + } - $size = getimagesize($root); - return new static($size[0] ?? 0, $size[1] ?? 1); - } + $size = getimagesize($root); + return new static($size[0] ?? 0, $size[1] ?? 1); + } - /** - * Detect the dimensions for a svg file - * - * @param string $root - * @return static - */ - public static function forSvg(string $root) - { - // avoid xml errors - libxml_use_internal_errors(true); + /** + * Detect the dimensions for a svg file + * + * @param string $root + * @return static + */ + public static function forSvg(string $root) + { + // avoid xml errors + libxml_use_internal_errors(true); - $content = file_get_contents($root); - $height = 0; - $width = 0; - $xml = simplexml_load_string($content); + $content = file_get_contents($root); + $height = 0; + $width = 0; + $xml = simplexml_load_string($content); - if ($xml !== false) { - $attr = $xml->attributes(); - $width = (int)($attr->width); - $height = (int)($attr->height); - if (($width === 0 || $height === 0) && empty($attr->viewBox) === false) { - $box = explode(' ', $attr->viewBox); - $width = (int)($box[2] ?? 0); - $height = (int)($box[3] ?? 0); - } - } + if ($xml !== false) { + $attr = $xml->attributes(); + $width = (int)($attr->width); + $height = (int)($attr->height); + if (($width === 0 || $height === 0) && empty($attr->viewBox) === false) { + $box = explode(' ', $attr->viewBox); + $width = (int)($box[2] ?? 0); + $height = (int)($box[3] ?? 0); + } + } - return new static($width, $height); - } + return new static($width, $height); + } - /** - * Checks if the dimensions are landscape - * - * @return bool - */ - public function landscape(): bool - { - return $this->width > $this->height; - } + /** + * Checks if the dimensions are landscape + * + * @return bool + */ + public function landscape(): bool + { + return $this->width > $this->height; + } - /** - * Returns a string representation of the orientation - * - * @return string|false - */ - public function orientation() - { - if (!$this->ratio()) { - return false; - } + /** + * Returns a string representation of the orientation + * + * @return string|false + */ + public function orientation() + { + if (!$this->ratio()) { + return false; + } - if ($this->portrait()) { - return 'portrait'; - } + if ($this->portrait()) { + return 'portrait'; + } - if ($this->landscape()) { - return 'landscape'; - } + if ($this->landscape()) { + return 'landscape'; + } - return 'square'; - } + return 'square'; + } - /** - * Checks if the dimensions are portrait - * - * @return bool - */ - public function portrait(): bool - { - return $this->height > $this->width; - } + /** + * Checks if the dimensions are portrait + * + * @return bool + */ + public function portrait(): bool + { + return $this->height > $this->width; + } - /** - * Calculates and returns the ratio - * - * - * - * $dimensions = new Dimensions(1200, 768); - * echo $dimensions->ratio(); - * // output: 1.5625 - * - * - * - * @return float - */ - public function ratio(): float - { - if ($this->width !== 0 && $this->height !== 0) { - return $this->width / $this->height; - } + /** + * Calculates and returns the ratio + * + * + * + * $dimensions = new Dimensions(1200, 768); + * echo $dimensions->ratio(); + * // output: 1.5625 + * + * + * + * @return float + */ + public function ratio(): float + { + if ($this->width !== 0 && $this->height !== 0) { + return $this->width / $this->height; + } - return 0; - } + return 0; + } - /** - * @param int|null $width - * @param int|null $height - * @param bool $force - * @return $this - */ - public function resize(int $width = null, int $height = null, bool $force = false) - { - return $this->fitWidthAndHeight($width, $height, $force); - } + /** + * @param int|null $width + * @param int|null $height + * @param bool $force + * @return $this + */ + public function resize(int $width = null, int $height = null, bool $force = false) + { + return $this->fitWidthAndHeight($width, $height, $force); + } - /** - * Checks if the dimensions are square - * - * @return bool - */ - public function square(): bool - { - return $this->width === $this->height; - } + /** + * Checks if the dimensions are square + * + * @return bool + */ + public function square(): bool + { + return $this->width === $this->height; + } - /** - * Resize and crop - * - * @param array $options - * @return $this - */ - public function thumb(array $options = []) - { - $width = $options['width'] ?? null; - $height = $options['height'] ?? null; - $crop = $options['crop'] ?? false; - $method = $crop !== false ? 'crop' : 'resize'; + /** + * Resize and crop + * + * @param array $options + * @return $this + */ + public function thumb(array $options = []) + { + $width = $options['width'] ?? null; + $height = $options['height'] ?? null; + $crop = $options['crop'] ?? false; + $method = $crop !== false ? 'crop' : 'resize'; - if ($width === null && $height === null) { - return $this; - } + if ($width === null && $height === null) { + return $this; + } - return $this->$method($width, $height); - } + return $this->$method($width, $height); + } - /** - * Converts the dimensions object - * to a plain PHP array - * - * @return array - */ - public function toArray(): array - { - return [ - 'width' => $this->width(), - 'height' => $this->height(), - 'ratio' => $this->ratio(), - 'orientation' => $this->orientation(), - ]; - } + /** + * Converts the dimensions object + * to a plain PHP array + * + * @return array + */ + public function toArray(): array + { + return [ + 'width' => $this->width(), + 'height' => $this->height(), + 'ratio' => $this->ratio(), + 'orientation' => $this->orientation(), + ]; + } - /** - * Returns the width - * - * @return int - */ - public function width(): int - { - return $this->width; - } + /** + * Returns the width + * + * @return int + */ + public function width(): int + { + return $this->width; + } } diff --git a/kirby/src/Image/Exif.php b/kirby/src/Image/Exif.php index 2dd120e..9405271 100644 --- a/kirby/src/Image/Exif.php +++ b/kirby/src/Image/Exif.php @@ -15,284 +15,284 @@ use Kirby\Toolkit\V; */ class Exif { - /** - * the parent image object - * @var \Kirby\Image\Image - */ - protected $image; + /** + * the parent image object + * @var \Kirby\Image\Image + */ + protected $image; - /** - * the raw exif array - * @var array - */ - protected $data = []; + /** + * the raw exif array + * @var array + */ + protected $data = []; - /** - * the camera object with model and make - * @var Camera - */ - protected $camera; + /** + * the camera object with model and make + * @var Camera + */ + protected $camera; - /** - * the location object - * @var Location - */ - protected $location; + /** + * the location object + * @var Location + */ + protected $location; - /** - * the timestamp - * - * @var string - */ - protected $timestamp; + /** + * the timestamp + * + * @var string + */ + protected $timestamp; - /** - * the exposure value - * - * @var string - */ - protected $exposure; + /** + * the exposure value + * + * @var string + */ + protected $exposure; - /** - * the aperture value - * - * @var string - */ - protected $aperture; + /** + * the aperture value + * + * @var string + */ + protected $aperture; - /** - * iso value - * - * @var string - */ - protected $iso; + /** + * iso value + * + * @var string + */ + protected $iso; - /** - * focal length - * - * @var string - */ - protected $focalLength; + /** + * focal length + * + * @var string + */ + protected $focalLength; - /** - * color or black/white - * @var bool - */ - protected $isColor; + /** + * color or black/white + * @var bool + */ + protected $isColor; - /** - * Constructor - * - * @param \Kirby\Image\Image $image - */ - public function __construct(Image $image) - { - $this->image = $image; - $this->data = $this->read(); - $this->parse(); - } + /** + * Constructor + * + * @param \Kirby\Image\Image $image + */ + public function __construct(Image $image) + { + $this->image = $image; + $this->data = $this->read(); + $this->parse(); + } - /** - * Returns the raw data array from the parser - * - * @return array - */ - public function data(): array - { - return $this->data; - } + /** + * Returns the raw data array from the parser + * + * @return array + */ + public function data(): array + { + return $this->data; + } - /** - * Returns the Camera object - * - * @return \Kirby\Image\Camera|null - */ - public function camera() - { - if ($this->camera !== null) { - return $this->camera; - } + /** + * Returns the Camera object + * + * @return \Kirby\Image\Camera|null + */ + public function camera() + { + if ($this->camera !== null) { + return $this->camera; + } - return $this->camera = new Camera($this->data); - } + return $this->camera = new Camera($this->data); + } - /** - * Returns the location object - * - * @return \Kirby\Image\Location|null - */ - public function location() - { - if ($this->location !== null) { - return $this->location; - } + /** + * Returns the location object + * + * @return \Kirby\Image\Location|null + */ + public function location() + { + if ($this->location !== null) { + return $this->location; + } - return $this->location = new Location($this->data); - } + return $this->location = new Location($this->data); + } - /** - * Returns the timestamp - * - * @return string|null - */ - public function timestamp() - { - return $this->timestamp; - } + /** + * Returns the timestamp + * + * @return string|null + */ + public function timestamp() + { + return $this->timestamp; + } - /** - * Returns the exposure - * - * @return string|null - */ - public function exposure() - { - return $this->exposure; - } + /** + * Returns the exposure + * + * @return string|null + */ + public function exposure() + { + return $this->exposure; + } - /** - * Returns the aperture - * - * @return string|null - */ - public function aperture() - { - return $this->aperture; - } + /** + * Returns the aperture + * + * @return string|null + */ + public function aperture() + { + return $this->aperture; + } - /** - * Returns the iso value - * - * @return int|null - */ - public function iso() - { - return $this->iso; - } + /** + * Returns the iso value + * + * @return int|null + */ + public function iso() + { + return $this->iso; + } - /** - * Checks if this is a color picture - * - * @return bool|null - */ - public function isColor() - { - return $this->isColor; - } + /** + * Checks if this is a color picture + * + * @return bool|null + */ + public function isColor() + { + return $this->isColor; + } - /** - * Checks if this is a bw picture - * - * @return bool|null - */ - public function isBW(): ?bool - { - return ($this->isColor !== null) ? $this->isColor === false : null; - } + /** + * Checks if this is a bw picture + * + * @return bool|null + */ + public function isBW(): ?bool + { + return ($this->isColor !== null) ? $this->isColor === false : null; + } - /** - * Returns the focal length - * - * @return string|null - */ - public function focalLength() - { - return $this->focalLength; - } + /** + * Returns the focal length + * + * @return string|null + */ + public function focalLength() + { + return $this->focalLength; + } - /** - * Read the exif data of the image object if possible - * - * @return mixed - */ - protected function read(): array - { - // @codeCoverageIgnoreStart - if (function_exists('exif_read_data') === false) { - return []; - } - // @codeCoverageIgnoreEnd + /** + * Read the exif data of the image object if possible + * + * @return mixed + */ + protected function read(): array + { + // @codeCoverageIgnoreStart + if (function_exists('exif_read_data') === false) { + return []; + } + // @codeCoverageIgnoreEnd - $data = @exif_read_data($this->image->root()); - return is_array($data) ? $data : []; - } + $data = @exif_read_data($this->image->root()); + return is_array($data) ? $data : []; + } - /** - * Get all computed data - * - * @return array - */ - protected function computed(): array - { - return $this->data['COMPUTED'] ?? []; - } + /** + * Get all computed data + * + * @return array + */ + protected function computed(): array + { + return $this->data['COMPUTED'] ?? []; + } - /** - * Pareses and stores all relevant exif data - */ - protected function parse() - { - $this->timestamp = $this->parseTimestamp(); - $this->exposure = $this->data['ExposureTime'] ?? null; - $this->iso = $this->data['ISOSpeedRatings'] ?? null; - $this->focalLength = $this->parseFocalLength(); - $this->aperture = $this->computed()['ApertureFNumber'] ?? null; - $this->isColor = V::accepted($this->computed()['IsColor'] ?? null); - } + /** + * Pareses and stores all relevant exif data + */ + protected function parse() + { + $this->timestamp = $this->parseTimestamp(); + $this->exposure = $this->data['ExposureTime'] ?? null; + $this->iso = $this->data['ISOSpeedRatings'] ?? null; + $this->focalLength = $this->parseFocalLength(); + $this->aperture = $this->computed()['ApertureFNumber'] ?? null; + $this->isColor = V::accepted($this->computed()['IsColor'] ?? null); + } - /** - * Return the timestamp when the picture has been taken - * - * @return string|int - */ - protected function parseTimestamp() - { - if (isset($this->data['DateTimeOriginal']) === true) { - return strtotime($this->data['DateTimeOriginal']); - } + /** + * Return the timestamp when the picture has been taken + * + * @return string|int + */ + protected function parseTimestamp() + { + if (isset($this->data['DateTimeOriginal']) === true) { + return strtotime($this->data['DateTimeOriginal']); + } - return $this->data['FileDateTime'] ?? $this->image->modified(); - } + return $this->data['FileDateTime'] ?? $this->image->modified(); + } - /** - * Return the focal length - * - * @return string|null - */ - protected function parseFocalLength() - { - return $this->data['FocalLength'] ?? $this->data['FocalLengthIn35mmFilm'] ?? null; - } + /** + * Return the focal length + * + * @return string|null + */ + protected function parseFocalLength() + { + return $this->data['FocalLength'] ?? $this->data['FocalLengthIn35mmFilm'] ?? null; + } - /** - * Converts the object into a nicely readable array - * - * @return array - */ - public function toArray(): array - { - return [ - 'camera' => $this->camera() ? $this->camera()->toArray() : null, - 'location' => $this->location() ? $this->location()->toArray() : null, - 'timestamp' => $this->timestamp(), - 'exposure' => $this->exposure(), - 'aperture' => $this->aperture(), - 'iso' => $this->iso(), - 'focalLength' => $this->focalLength(), - 'isColor' => $this->isColor() - ]; - } + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray(): array + { + return [ + 'camera' => $this->camera() ? $this->camera()->toArray() : null, + 'location' => $this->location() ? $this->location()->toArray() : null, + 'timestamp' => $this->timestamp(), + 'exposure' => $this->exposure(), + 'aperture' => $this->aperture(), + 'iso' => $this->iso(), + 'focalLength' => $this->focalLength(), + 'isColor' => $this->isColor() + ]; + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return array_merge($this->toArray(), [ - 'camera' => $this->camera(), - 'location' => $this->location() - ]); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return array_merge($this->toArray(), [ + 'camera' => $this->camera(), + 'location' => $this->location() + ]); + } } diff --git a/kirby/src/Image/Image.php b/kirby/src/Image/Image.php index f50471f..b505a9a 100644 --- a/kirby/src/Image/Image.php +++ b/kirby/src/Image/Image.php @@ -22,230 +22,230 @@ use Kirby\Toolkit\Html; */ class Image extends File { - /** - * @var \Kirby\Image\Exif|null - */ - protected $exif; + /** + * @var \Kirby\Image\Exif|null + */ + protected $exif; - /** - * @var \Kirby\Image\Dimensions|null - */ - protected $dimensions; + /** + * @var \Kirby\Image\Dimensions|null + */ + protected $dimensions; - /** - * @var array - */ - public static $resizableTypes = [ - 'jpg', - 'jpeg', - 'gif', - 'png', - 'webp' - ]; + /** + * @var array + */ + public static $resizableTypes = [ + 'jpg', + 'jpeg', + 'gif', + 'png', + 'webp' + ]; - /** - * @var array - */ - public static $viewableTypes = [ - 'avif', - 'jpg', - 'jpeg', - 'gif', - 'png', - 'svg', - 'webp' - ]; + /** + * @var array + */ + public static $viewableTypes = [ + 'avif', + 'jpg', + 'jpeg', + 'gif', + 'png', + 'svg', + 'webp' + ]; - /** - * Validation rules to be used for `::match()` - * - * @var array - */ - public static $validations = [ - 'maxsize' => ['size', 'max'], - 'minsize' => ['size', 'min'], - 'maxwidth' => ['width', 'max'], - 'minwidth' => ['width', 'min'], - 'maxheight' => ['height', 'max'], - 'minheight' => ['height', 'min'], - 'orientation' => ['orientation', 'same'] - ]; + /** + * Validation rules to be used for `::match()` + * + * @var array + */ + public static $validations = [ + 'maxsize' => ['size', 'max'], + 'minsize' => ['size', 'min'], + 'maxwidth' => ['width', 'max'], + 'minwidth' => ['width', 'min'], + 'maxheight' => ['height', 'max'], + 'minheight' => ['height', 'min'], + 'orientation' => ['orientation', 'same'] + ]; - /** - * Returns the `` tag for the image object - * - * @return string - */ - public function __toString(): string - { - return $this->html(); - } + /** + * Returns the `` tag for the image object + * + * @return string + */ + public function __toString(): string + { + return $this->html(); + } - /** - * Returns the dimensions of the file if possible - * - * @return \Kirby\Image\Dimensions - */ - public function dimensions() - { - if ($this->dimensions !== null) { - return $this->dimensions; - } + /** + * Returns the dimensions of the file if possible + * + * @return \Kirby\Image\Dimensions + */ + public function dimensions() + { + if ($this->dimensions !== null) { + return $this->dimensions; + } - if (in_array($this->mime(), [ - 'image/jpeg', - 'image/jp2', - 'image/png', - 'image/gif', - 'image/webp' - ])) { - return $this->dimensions = Dimensions::forImage($this->root); - } + if (in_array($this->mime(), [ + 'image/jpeg', + 'image/jp2', + 'image/png', + 'image/gif', + 'image/webp' + ])) { + return $this->dimensions = Dimensions::forImage($this->root); + } - if ($this->extension() === 'svg') { - return $this->dimensions = Dimensions::forSvg($this->root); - } + if ($this->extension() === 'svg') { + return $this->dimensions = Dimensions::forSvg($this->root); + } - return $this->dimensions = new Dimensions(0, 0); - } + return $this->dimensions = new Dimensions(0, 0); + } - /** - * Returns the exif object for this file (if image) - * - * @return \Kirby\Image\Exif - */ - public function exif() - { - return $this->exif ??= new Exif($this); - } + /** + * Returns the exif object for this file (if image) + * + * @return \Kirby\Image\Exif + */ + public function exif() + { + return $this->exif ??= new Exif($this); + } - /** - * Returns the height of the asset - * - * @return int - */ - public function height(): int - { - return $this->dimensions()->height(); - } + /** + * Returns the height of the asset + * + * @return int + */ + public function height(): int + { + return $this->dimensions()->height(); + } - /** - * Converts the file to html - * - * @param array $attr - * @return string - */ - public function html(array $attr = []): string - { - return Html::img($this->url(), $attr); - } + /** + * Converts the file to html + * + * @param array $attr + * @return string + */ + public function html(array $attr = []): string + { + return Html::img($this->url(), $attr); + } - /** - * Returns the PHP imagesize array - * - * @return array - */ - public function imagesize(): array - { - return getimagesize($this->root); - } + /** + * Returns the PHP imagesize array + * + * @return array + */ + public function imagesize(): array + { + return getimagesize($this->root); + } - /** - * Checks if the dimensions of the asset are portrait - * - * @return bool - */ - public function isPortrait(): bool - { - return $this->dimensions()->portrait(); - } + /** + * Checks if the dimensions of the asset are portrait + * + * @return bool + */ + public function isPortrait(): bool + { + return $this->dimensions()->portrait(); + } - /** - * Checks if the dimensions of the asset are landscape - * - * @return bool - */ - public function isLandscape(): bool - { - return $this->dimensions()->landscape(); - } + /** + * Checks if the dimensions of the asset are landscape + * + * @return bool + */ + public function isLandscape(): bool + { + return $this->dimensions()->landscape(); + } - /** - * Checks if the dimensions of the asset are square - * - * @return bool - */ - public function isSquare(): bool - { - return $this->dimensions()->square(); - } + /** + * Checks if the dimensions of the asset are square + * + * @return bool + */ + public function isSquare(): bool + { + return $this->dimensions()->square(); + } - /** - * Checks if the file is a resizable image - * - * @return bool - */ - public function isResizable(): bool - { - return in_array($this->extension(), static::$resizableTypes) === true; - } + /** + * Checks if the file is a resizable image + * + * @return bool + */ + public function isResizable(): bool + { + return in_array($this->extension(), static::$resizableTypes) === true; + } - /** - * Checks if a preview can be displayed for the file - * in the Panel or in the frontend - * - * @return bool - */ - public function isViewable(): bool - { - return in_array($this->extension(), static::$viewableTypes) === true; - } + /** + * Checks if a preview can be displayed for the file + * in the Panel or in the frontend + * + * @return bool + */ + public function isViewable(): bool + { + return in_array($this->extension(), static::$viewableTypes) === true; + } - /** - * Returns the ratio of the asset - * - * @return float - */ - public function ratio(): float - { - return $this->dimensions()->ratio(); - } + /** + * Returns the ratio of the asset + * + * @return float + */ + public function ratio(): float + { + return $this->dimensions()->ratio(); + } - /** - * Returns the orientation as string - * landscape | portrait | square - * - * @return string - */ - public function orientation(): string - { - return $this->dimensions()->orientation(); - } + /** + * Returns the orientation as string + * landscape | portrait | square + * + * @return string + */ + public function orientation(): string + { + return $this->dimensions()->orientation(); + } - /** - * Converts the object to an array - * - * @return array - */ - public function toArray(): array - { - $array = array_merge(parent::toArray(), [ - 'dimensions' => $this->dimensions()->toArray(), - 'exif' => $this->exif()->toArray(), - ]); + /** + * Converts the object to an array + * + * @return array + */ + public function toArray(): array + { + $array = array_merge(parent::toArray(), [ + 'dimensions' => $this->dimensions()->toArray(), + 'exif' => $this->exif()->toArray(), + ]); - ksort($array); + ksort($array); - return $array; - } + return $array; + } - /** - * Returns the width of the asset - * - * @return int - */ - public function width(): int - { - return $this->dimensions()->width(); - } + /** + * Returns the width of the asset + * + * @return int + */ + public function width(): int + { + return $this->dimensions()->width(); + } } diff --git a/kirby/src/Image/Location.php b/kirby/src/Image/Location.php index b038ca2..00b65c4 100644 --- a/kirby/src/Image/Location.php +++ b/kirby/src/Image/Location.php @@ -14,123 +14,123 @@ namespace Kirby\Image; */ class Location { - /** - * latitude - * - * @var float|null - */ - protected $lat; + /** + * latitude + * + * @var float|null + */ + protected $lat; - /** - * longitude - * - * @var float|null - */ - protected $lng; + /** + * longitude + * + * @var float|null + */ + protected $lng; - /** - * Constructor - * - * @param array $exif The entire exif array - */ - public function __construct(array $exif) - { - if (isset($exif['GPSLatitude']) === true && - isset($exif['GPSLatitudeRef']) === true && - isset($exif['GPSLongitude']) === true && - isset($exif['GPSLongitudeRef']) === true - ) { - $this->lat = $this->gps($exif['GPSLatitude'], $exif['GPSLatitudeRef']); - $this->lng = $this->gps($exif['GPSLongitude'], $exif['GPSLongitudeRef']); - } - } + /** + * Constructor + * + * @param array $exif The entire exif array + */ + public function __construct(array $exif) + { + if (isset($exif['GPSLatitude']) === true && + isset($exif['GPSLatitudeRef']) === true && + isset($exif['GPSLongitude']) === true && + isset($exif['GPSLongitudeRef']) === true + ) { + $this->lat = $this->gps($exif['GPSLatitude'], $exif['GPSLatitudeRef']); + $this->lng = $this->gps($exif['GPSLongitude'], $exif['GPSLongitudeRef']); + } + } - /** - * Returns the latitude - * - * @return float|null - */ - public function lat() - { - return $this->lat; - } + /** + * Returns the latitude + * + * @return float|null + */ + public function lat() + { + return $this->lat; + } - /** - * Returns the longitude - * - * @return float|null - */ - public function lng() - { - return $this->lng; - } + /** + * Returns the longitude + * + * @return float|null + */ + public function lng() + { + return $this->lng; + } - /** - * Converts the gps coordinates - * - * @param string|array $coord - * @param string $hemi - * @return float - */ - protected function gps($coord, string $hemi): float - { - $degrees = count($coord) > 0 ? $this->num($coord[0]) : 0; - $minutes = count($coord) > 1 ? $this->num($coord[1]) : 0; - $seconds = count($coord) > 2 ? $this->num($coord[2]) : 0; + /** + * Converts the gps coordinates + * + * @param string|array $coord + * @param string $hemi + * @return float + */ + protected function gps($coord, string $hemi): float + { + $degrees = count($coord) > 0 ? $this->num($coord[0]) : 0; + $minutes = count($coord) > 1 ? $this->num($coord[1]) : 0; + $seconds = count($coord) > 2 ? $this->num($coord[2]) : 0; - $hemi = strtoupper($hemi); - $flip = ($hemi === 'W' || $hemi === 'S') ? -1 : 1; + $hemi = strtoupper($hemi); + $flip = ($hemi === 'W' || $hemi === 'S') ? -1 : 1; - return $flip * ($degrees + $minutes / 60 + $seconds / 3600); - } + return $flip * ($degrees + $minutes / 60 + $seconds / 3600); + } - /** - * Converts coordinates to floats - * - * @param string $part - * @return float - */ - protected function num(string $part): float - { - $parts = explode('/', $part); + /** + * Converts coordinates to floats + * + * @param string $part + * @return float + */ + protected function num(string $part): float + { + $parts = explode('/', $part); - if (count($parts) === 1) { - return (float)$parts[0]; - } + if (count($parts) === 1) { + return (float)$parts[0]; + } - return (float)($parts[0]) / (float)($parts[1]); - } + return (float)($parts[0]) / (float)($parts[1]); + } - /** - * Converts the object into a nicely readable array - * - * @return array - */ - public function toArray(): array - { - return [ - 'lat' => $this->lat(), - 'lng' => $this->lng() - ]; - } + /** + * Converts the object into a nicely readable array + * + * @return array + */ + public function toArray(): array + { + return [ + 'lat' => $this->lat(), + 'lng' => $this->lng() + ]; + } - /** - * Echos the entire location as lat, lng - * - * @return string - */ - public function __toString(): string - { - return trim(trim($this->lat() . ', ' . $this->lng(), ',')); - } + /** + * Echos the entire location as lat, lng + * + * @return string + */ + public function __toString(): string + { + return trim(trim($this->lat() . ', ' . $this->lng(), ',')); + } - /** - * Improved `var_dump` output - * - * @return array - */ - public function __debugInfo(): array - { - return $this->toArray(); - } + /** + * Improved `var_dump` output + * + * @return array + */ + public function __debugInfo(): array + { + return $this->toArray(); + } } diff --git a/kirby/src/Panel/Dialog.php b/kirby/src/Panel/Dialog.php index cdee07f..a7825e9 100644 --- a/kirby/src/Panel/Dialog.php +++ b/kirby/src/Panel/Dialog.php @@ -16,24 +16,24 @@ namespace Kirby\Panel; */ class Dialog extends Json { - protected static $key = '$dialog'; + protected static $key = '$dialog'; - /** - * Renders dialogs - * - * @param mixed $data - * @param array $options - * @return \Kirby\Http\Response - */ - public static function response($data, array $options = []) - { - // interpret true as success - if ($data === true) { - $data = [ - 'code' => 200 - ]; - } + /** + * Renders dialogs + * + * @param mixed $data + * @param array $options + * @return \Kirby\Http\Response + */ + public static function response($data, array $options = []) + { + // interpret true as success + if ($data === true) { + $data = [ + 'code' => 200 + ]; + } - return parent::response($data, $options); - } + return parent::response($data, $options); + } } diff --git a/kirby/src/Panel/Document.php b/kirby/src/Panel/Document.php index 2010e94..68e1f9c 100644 --- a/kirby/src/Panel/Document.php +++ b/kirby/src/Panel/Document.php @@ -2,8 +2,11 @@ namespace Kirby\Panel; +use Kirby\Cms\App; +use Kirby\Cms\Helpers; use Kirby\Exception\Exception; use Kirby\Exception\InvalidArgumentException; +use Kirby\Filesystem\Asset; use Kirby\Filesystem\Dir; use Kirby\Filesystem\F; use Kirby\Http\Response; @@ -25,272 +28,278 @@ use Throwable; */ class Document { - /** - * Generates an array with all assets - * that need to be loaded for the panel (js, css, icons) - * - * @return array - */ - public static function assets(): array - { - $kirby = kirby(); - $nonce = $kirby->nonce(); + /** + * Generates an array with all assets + * that need to be loaded for the panel (js, css, icons) + * + * @return array + */ + public static function assets(): array + { + $kirby = App::instance(); + $nonce = $kirby->nonce(); - // get the assets from the Vite dev server in dev mode; - // dev mode = explicitly enabled in the config AND Vite is running - $dev = $kirby->option('panel.dev', false); - $isDev = $dev !== false && is_file($kirby->roots()->panel() . '/.vite-running') === true; + // get the assets from the Vite dev server in dev mode; + // dev mode = explicitly enabled in the config AND Vite is running + $dev = $kirby->option('panel.dev', false); + $isDev = $dev !== false && is_file($kirby->roots()->panel() . '/.vite-running') === true; - if ($isDev === true) { - // vite on explicitly configured base URL or port 3000 - // of the current Kirby request - if (is_string($dev) === true) { - $url = $dev; - } else { - $url = rtrim($kirby->request()->url([ - 'port' => 3000, - 'path' => null, - 'params' => null, - 'query' => null - ])->toString(), '/'); - } - } else { - // vite is not running, use production assets - $url = $kirby->url('media') . '/panel/' . $kirby->versionHash(); - } + if ($isDev === true) { + // vite on explicitly configured base URL or port 3000 + // of the current Kirby request + if (is_string($dev) === true) { + $url = $dev; + } else { + $url = rtrim($kirby->request()->url([ + 'port' => 3000, + 'path' => null, + 'params' => null, + 'query' => null + ])->toString(), '/'); + } + } else { + // vite is not running, use production assets + $url = $kirby->url('media') . '/panel/' . $kirby->versionHash(); + } - // fetch all plugins - $plugins = new Plugins(); + // fetch all plugins + $plugins = new Plugins(); - $assets = [ - 'css' => [ - 'index' => $url . '/css/style.css', - 'plugins' => $plugins->url('css'), - 'custom' => static::customAsset('panel.css'), - ], - 'icons' => static::favicon($url), - 'js' => [ - 'vendor' => [ - 'nonce' => $nonce, - 'src' => $url . '/js/vendor.js', - 'type' => 'module' - ], - 'pluginloader' => [ - 'nonce' => $nonce, - 'src' => $url . '/js/plugins.js', - 'type' => 'module' - ], - 'plugins' => [ - 'nonce' => $nonce, - 'src' => $plugins->url('js'), - 'defer' => true - ], - 'custom' => [ - 'nonce' => $nonce, - 'src' => static::customAsset('panel.js'), - 'type' => 'module' - ], - 'index' => [ - 'nonce' => $nonce, - 'src' => $url . '/js/index.js', - 'type' => 'module' - ], - ] - ]; + $assets = [ + 'css' => [ + 'index' => $url . '/css/style.css', + 'plugins' => $plugins->url('css'), + 'custom' => static::customAsset('panel.css'), + ], + 'icons' => static::favicon($url), + // loader for plugins' index.dev.mjs files – inlined, so we provide the code instead of the asset URL + 'plugin-imports' => $plugins->read('mjs'), + 'js' => [ + 'vendor' => [ + 'nonce' => $nonce, + 'src' => $url . '/js/vendor.js', + 'type' => 'module' + ], + 'pluginloader' => [ + 'nonce' => $nonce, + 'src' => $url . '/js/plugins.js', + 'type' => 'module' + ], + 'plugins' => [ + 'nonce' => $nonce, + 'src' => $plugins->url('js'), + 'defer' => true + ], + 'custom' => [ + 'nonce' => $nonce, + 'src' => static::customAsset('panel.js'), + 'type' => 'module' + ], + 'index' => [ + 'nonce' => $nonce, + 'src' => $url . '/js/index.js', + 'type' => 'module' + ], + ] + ]; - // during dev mode, add vite client and adapt - // path to `index.js` - vendor and stylesheet - // don't need to be loaded in dev mode - if ($isDev === true) { - $assets['js']['vite'] = [ - 'nonce' => $nonce, - 'src' => $url . '/@vite/client', - 'type' => 'module' - ]; - $assets['js']['index'] = [ - 'nonce' => $nonce, - 'src' => $url . '/src/index.js', - 'type' => 'module' - ]; + // during dev mode, add vite client and adapt + // path to `index.js` - vendor and stylesheet + // don't need to be loaded in dev mode + if ($isDev === true) { + $assets['js']['vite'] = [ + 'nonce' => $nonce, + 'src' => $url . '/@vite/client', + 'type' => 'module' + ]; + $assets['js']['index'] = [ + 'nonce' => $nonce, + 'src' => $url . '/src/index.js', + 'type' => 'module' + ]; - unset($assets['css']['index'], $assets['js']['vendor']); - } + unset($assets['css']['index'], $assets['js']['vendor']); + } - // remove missing files - $assets['css'] = array_filter($assets['css']); - $assets['js'] = array_filter( - $assets['js'], - fn ($js) => empty($js['src']) === false - ); + // remove missing files + $assets['css'] = array_filter($assets['css']); + $assets['js'] = array_filter( + $assets['js'], + fn ($js) => empty($js['src']) === false + ); - return $assets; - } + return $assets; + } - /** - * Check for a custom asset file from the - * config (e.g. panel.css or panel.js) - * @since 3.6.2 - * - * @param string $option asset option name - * @return string|null - */ - public static function customAsset(string $option): ?string - { - if ($path = kirby()->option($option)) { - $asset = asset($path); + /** + * Check for a custom asset file from the + * config (e.g. panel.css or panel.js) + * @since 3.7.0 + * + * @param string $option asset option name + * @return string|null + */ + public static function customAsset(string $option): ?string + { + if ($path = App::instance()->option($option)) { + $asset = new Asset($path); - if ($asset->exists() === true) { - return $asset->url() . '?' . $asset->modified(); - } - } + if ($asset->exists() === true) { + return $asset->url() . '?' . $asset->modified(); + } + } - return null; - } + return null; + } - /** - * @deprecated 3.7.0 Use `Document::customAsset('panel.css)` instead - * @todo add deprecation warning in 3.7.0, remove in 3.8.0 - */ - public static function customCss(): ?string - { - return static::customAsset('panel.css'); - } + /** + * @deprecated 3.7.0 Use `Document::customAsset('panel.css)` instead + * @todo remove in 3.8.0 + * @codeCoverageIgnore + */ + public static function customCss(): ?string + { + Helpers::deprecated('Panel\Document::customCss() has been deprecated and will be removed in Kirby 3.8.0. Use Panel\Document::customAsset(\'panel.css\') instead.'); + return static::customAsset('panel.css'); + } - /** - * @deprecated 3.7.0 Use `Document::customAsset('panel.js)` instead - * @todo add deprecation warning in 3.7.0, remove in 3.8.0 - */ - public static function customJs(): ?string - { - return static::customAsset('panel.js'); - } + /** + * @deprecated 3.7.0 Use `Document::customAsset('panel.js)` instead + * @todo remove in 3.8.0 + * @codeCoverageIgnore + */ + public static function customJs(): ?string + { + Helpers::deprecated('Panel\Document::customJs() has been deprecated and will be removed in Kirby 3.8.0. Use Panel\Document::customAsset(\'panel.js\') instead.'); + return static::customAsset('panel.js'); + } - /** - * Returns array of favion icons - * based on config option - * @since 3.6.2 - * - * @param string $url URL prefix for default icons - * @return array - */ - public static function favicon(string $url = ''): array - { - $kirby = kirby(); - $icons = $kirby->option('panel.favicon', [ - 'apple-touch-icon' => [ - 'type' => 'image/png', - 'url' => $url . '/apple-touch-icon.png', - ], - 'shortcut icon' => [ - 'type' => 'image/svg+xml', - 'url' => $url . '/favicon.svg', - ], - 'alternate icon' => [ - 'type' => 'image/png', - 'url' => $url . '/favicon.png', - ] - ]); + /** + * Returns array of favion icons + * based on config option + * @since 3.7.0 + * + * @param string $url URL prefix for default icons + * @return array + */ + public static function favicon(string $url = ''): array + { + $kirby = App::instance(); + $icons = $kirby->option('panel.favicon', [ + 'apple-touch-icon' => [ + 'type' => 'image/png', + 'url' => $url . '/apple-touch-icon.png', + ], + 'shortcut icon' => [ + 'type' => 'image/svg+xml', + 'url' => $url . '/favicon.svg', + ], + 'alternate icon' => [ + 'type' => 'image/png', + 'url' => $url . '/favicon.png', + ] + ]); - if (is_array($icons) === true) { - return $icons; - } + if (is_array($icons) === true) { + return $icons; + } - // make sure to convert favicon string to array - if (is_string($icons) === true) { - return [ - 'shortcut icon' => [ - 'type' => F::mime($icons), - 'url' => $icons, - ] - ]; - } + // make sure to convert favicon string to array + if (is_string($icons) === true) { + return [ + 'shortcut icon' => [ + 'type' => F::mime($icons), + 'url' => $icons, + ] + ]; + } - throw new InvalidArgumentException('Invalid panel.favicon option'); - } + throw new InvalidArgumentException('Invalid panel.favicon option'); + } - /** - * Load the SVG icon sprite - * This will be injected in the - * initial HTML document for the Panel - * - * @return string - */ - public static function icons(): string - { - return F::read(kirby()->root('kirby') . '/panel/dist/img/icons.svg'); - } + /** + * Load the SVG icon sprite + * This will be injected in the + * initial HTML document for the Panel + * + * @return string + */ + public static function icons(): string + { + return F::read(App::instance()->root('kirby') . '/panel/dist/img/icons.svg'); + } - /** - * Links all dist files in the media folder - * and returns the link to the requested asset - * - * @return bool - * @throws \Kirby\Exception\Exception If Panel assets could not be moved to the public directory - */ - public static function link(): bool - { - $kirby = kirby(); - $mediaRoot = $kirby->root('media') . '/panel'; - $panelRoot = $kirby->root('panel') . '/dist'; - $versionHash = $kirby->versionHash(); - $versionRoot = $mediaRoot . '/' . $versionHash; + /** + * Links all dist files in the media folder + * and returns the link to the requested asset + * + * @return bool + * @throws \Kirby\Exception\Exception If Panel assets could not be moved to the public directory + */ + public static function link(): bool + { + $kirby = App::instance(); + $mediaRoot = $kirby->root('media') . '/panel'; + $panelRoot = $kirby->root('panel') . '/dist'; + $versionHash = $kirby->versionHash(); + $versionRoot = $mediaRoot . '/' . $versionHash; - // check if the version already exists - if (is_dir($versionRoot) === true) { - return false; - } + // check if the version already exists + if (is_dir($versionRoot) === true) { + return false; + } - // delete the panel folder and all previous versions - Dir::remove($mediaRoot); + // delete the panel folder and all previous versions + Dir::remove($mediaRoot); - // recreate the panel folder - Dir::make($mediaRoot, true); + // recreate the panel folder + Dir::make($mediaRoot, true); - // copy assets to the dist folder - if (Dir::copy($panelRoot, $versionRoot) !== true) { - throw new Exception('Panel assets could not be linked'); - } + // copy assets to the dist folder + if (Dir::copy($panelRoot, $versionRoot) !== true) { + throw new Exception('Panel assets could not be linked'); + } - return true; - } + return true; + } - /** - * Renders the panel document - * - * @param array $fiber - * @return \Kirby\Http\Response - */ - public static function response(array $fiber) - { - $kirby = kirby(); + /** + * Renders the panel document + * + * @param array $fiber + * @return \Kirby\Http\Response + */ + public static function response(array $fiber) + { + $kirby = App::instance(); - // Full HTML response - // @codeCoverageIgnoreStart - try { - if (static::link() === true) { - usleep(1); - go($kirby->url('index') . '/' . $kirby->path()); - } - } catch (Throwable $e) { - die('The Panel assets cannot be installed properly. ' . $e->getMessage()); - } - // @codeCoverageIgnoreEnd + // Full HTML response + // @codeCoverageIgnoreStart + try { + if (static::link() === true) { + usleep(1); + Response::go($kirby->url('base') . '/' . $kirby->path()); + } + } catch (Throwable $e) { + die('The Panel assets cannot be installed properly. ' . $e->getMessage()); + } + // @codeCoverageIgnoreEnd - // get the uri object for the panel url - $uri = new Uri($url = $kirby->url('panel')); + // get the uri object for the panel url + $uri = new Uri($url = $kirby->url('panel')); - // proper response code - $code = $fiber['$view']['code'] ?? 200; + // proper response code + $code = $fiber['$view']['code'] ?? 200; - // load the main Panel view template - $body = Tpl::load($kirby->root('kirby') . '/views/panel.php', [ - 'assets' => static::assets(), - 'icons' => static::icons(), - 'nonce' => $kirby->nonce(), - 'fiber' => $fiber, - 'panelUrl' => $uri->path()->toString(true) . '/', - ]); + // load the main Panel view template + $body = Tpl::load($kirby->root('kirby') . '/views/panel.php', [ + 'assets' => static::assets(), + 'icons' => static::icons(), + 'nonce' => $kirby->nonce(), + 'fiber' => $fiber, + 'panelUrl' => $uri->path()->toString(true) . '/', + ]); - return new Response($body, 'text/html', $code); - } + return new Response($body, 'text/html', $code); + } } diff --git a/kirby/src/Panel/Dropdown.php b/kirby/src/Panel/Dropdown.php index 0d92a0c..42bdd91 100644 --- a/kirby/src/Panel/Dropdown.php +++ b/kirby/src/Panel/Dropdown.php @@ -2,6 +2,7 @@ namespace Kirby\Panel; +use Kirby\Cms\App; use Kirby\Cms\Find; use Kirby\Exception\LogicException; use Kirby\Http\Uri; @@ -22,68 +23,68 @@ use Throwable; */ class Dropdown extends Json { - protected static $key = '$dropdown'; + protected static $key = '$dropdown'; - /** - * Returns the options for the changes dropdown - * - * @return array - */ - public static function changes(): array - { - $kirby = kirby(); - $multilang = $kirby->multilang(); - $ids = Str::split(get('ids')); - $options = []; + /** + * Returns the options for the changes dropdown + * + * @return array + */ + public static function changes(): array + { + $kirby = App::instance(); + $multilang = $kirby->multilang(); + $ids = Str::split($kirby->request()->get('ids')); + $options = []; - foreach ($ids as $id) { - try { - // parse the given ID to extract - // the path and an optional query - $uri = new Uri($id); - $path = $uri->path()->toString(); - $query = $uri->query(); - $option = Find::parent($path)->panel()->dropdownOption(); + foreach ($ids as $id) { + try { + // parse the given ID to extract + // the path and an optional query + $uri = new Uri($id); + $path = $uri->path()->toString(); + $query = $uri->query(); + $option = Find::parent($path)->panel()->dropdownOption(); - // add the language to each option, if it is included in the query - // of the given ID and the language actually exists - if ($multilang && $query->language && $language = $kirby->language($query->language)) { - $option['text'] .= ' (' . $language->code() . ')'; - $option['link'] .= '?language=' . $language->code(); - } + // add the language to each option, if it is included in the query + // of the given ID and the language actually exists + if ($multilang && $query->language && $language = $kirby->language($query->language)) { + $option['text'] .= ' (' . $language->code() . ')'; + $option['link'] .= '?language=' . $language->code(); + } - $options[] = $option; - } catch (Throwable $e) { - continue; - } - } + $options[] = $option; + } catch (Throwable $e) { + continue; + } + } - // the given set of ids does not match any - // real models. This means that the stored ids - // in local storage are not correct and the changes - // store needs to be cleared - if (empty($options) === true) { - throw new LogicException('No changes for given models'); - } + // the given set of ids does not match any + // real models. This means that the stored ids + // in local storage are not correct and the changes + // store needs to be cleared + if (empty($options) === true) { + throw new LogicException('No changes for given models'); + } - return $options; - } + return $options; + } - /** - * Renders dropdowns - * - * @param mixed $data - * @param array $options - * @return \Kirby\Http\Response - */ - public static function response($data, array $options = []) - { - if (is_array($data) === true) { - $data = [ - 'options' => array_values($data) - ]; - } + /** + * Renders dropdowns + * + * @param mixed $data + * @param array $options + * @return \Kirby\Http\Response + */ + public static function response($data, array $options = []) + { + if (is_array($data) === true) { + $data = [ + 'options' => array_values($data) + ]; + } - return parent::response($data, $options); - } + return parent::response($data, $options); + } } diff --git a/kirby/src/Panel/Field.php b/kirby/src/Panel/Field.php index 6317a45..3d02d21 100644 --- a/kirby/src/Panel/Field.php +++ b/kirby/src/Panel/Field.php @@ -2,8 +2,10 @@ namespace Kirby\Panel; +use Kirby\Cms\App; use Kirby\Cms\File; use Kirby\Cms\Page; +use Kirby\Toolkit\I18n; /** * Provides common field prop definitions @@ -18,255 +20,255 @@ use Kirby\Cms\Page; */ class Field { - /** - * A standard email field - * - * @param array $props - * @return array - */ - public static function email(array $props = []): array - { - return array_merge([ - 'label' => t('email'), - 'type' => 'email', - 'counter' => false, - ], $props); - } + /** + * A standard email field + * + * @param array $props + * @return array + */ + public static function email(array $props = []): array + { + return array_merge([ + 'label' => I18n::translate('email'), + 'type' => 'email', + 'counter' => false, + ], $props); + } - /** - * File position - * - * @param \Kirby\Cms\File - * @param array $props - * @return array - */ - public static function filePosition(File $file, array $props = []): array - { - $index = 0; - $options = []; + /** + * File position + * + * @param \Kirby\Cms\File + * @param array $props + * @return array + */ + public static function filePosition(File $file, array $props = []): array + { + $index = 0; + $options = []; - foreach ($file->siblings(false)->sorted() as $sibling) { - $index++; + foreach ($file->siblings(false)->sorted() as $sibling) { + $index++; - $options[] = [ - 'value' => $index, - 'text' => $index - ]; + $options[] = [ + 'value' => $index, + 'text' => $index + ]; - $options[] = [ - 'value' => $sibling->id(), - 'text' => $sibling->filename(), - 'disabled' => true - ]; - } + $options[] = [ + 'value' => $sibling->id(), + 'text' => $sibling->filename(), + 'disabled' => true + ]; + } - $index++; + $index++; - $options[] = [ - 'value' => $index, - 'text' => $index - ]; + $options[] = [ + 'value' => $index, + 'text' => $index + ]; - return array_merge([ - 'label' => t('file.sort'), - 'type' => 'select', - 'empty' => false, - 'options' => $options - ], $props); - } + return array_merge([ + 'label' => I18n::translate('file.sort'), + 'type' => 'select', + 'empty' => false, + 'options' => $options + ], $props); + } - /** - * @return array - */ - public static function hidden(): array - { - return ['type' => 'hidden']; - } + /** + * @return array + */ + public static function hidden(): array + { + return ['type' => 'hidden']; + } - /** - * Page position - * - * @param \Kirby\Cms\Page - * @param array $props - * @return array - */ - public static function pagePosition(Page $page, array $props = []): array - { - $index = 0; - $options = []; - $siblings = $page->parentModel()->children()->listed()->not($page); + /** + * Page position + * + * @param \Kirby\Cms\Page + * @param array $props + * @return array + */ + public static function pagePosition(Page $page, array $props = []): array + { + $index = 0; + $options = []; + $siblings = $page->parentModel()->children()->listed()->not($page); - foreach ($siblings as $sibling) { - $index++; + foreach ($siblings as $sibling) { + $index++; - $options[] = [ - 'value' => $index, - 'text' => $index - ]; + $options[] = [ + 'value' => $index, + 'text' => $index + ]; - $options[] = [ - 'value' => $sibling->id(), - 'text' => $sibling->title()->value(), - 'disabled' => true - ]; - } + $options[] = [ + 'value' => $sibling->id(), + 'text' => $sibling->title()->value(), + 'disabled' => true + ]; + } - $index++; + $index++; - $options[] = [ - 'value' => $index, - 'text' => $index - ]; + $options[] = [ + 'value' => $index, + 'text' => $index + ]; - // if only one available option, - // hide field when not in debug mode - if (count($options) < 2) { - return static::hidden(); - } + // if only one available option, + // hide field when not in debug mode + if (count($options) < 2) { + return static::hidden(); + } - return array_merge([ - 'label' => t('page.changeStatus.position'), - 'type' => 'select', - 'empty' => false, - 'options' => $options, - ], $props); - } + return array_merge([ + 'label' => I18n::translate('page.changeStatus.position'), + 'type' => 'select', + 'empty' => false, + 'options' => $options, + ], $props); + } - /** - * A regular password field - * - * @param array $props - * @return array - */ - public static function password(array $props = []): array - { - return array_merge([ - 'label' => t('password'), - 'type' => 'password' - ], $props); - } + /** + * A regular password field + * + * @param array $props + * @return array + */ + public static function password(array $props = []): array + { + return array_merge([ + 'label' => I18n::translate('password'), + 'type' => 'password' + ], $props); + } - /** - * User role radio buttons - * - * @param array $props - * @return array - */ - public static function role(array $props = []): array - { - $kirby = kirby(); - $user = $kirby->user(); - $isAdmin = $user && $user->isAdmin(); - $roles = []; + /** + * User role radio buttons + * + * @param array $props + * @return array + */ + public static function role(array $props = []): array + { + $kirby = App::instance(); + $user = $kirby->user(); + $isAdmin = $user && $user->isAdmin(); + $roles = []; - foreach ($kirby->roles() as $role) { - // exclude the admin role, if the user - // is not allowed to change role to admin - if ($role->name() === 'admin' && $isAdmin === false) { - continue; - } + foreach ($kirby->roles() as $role) { + // exclude the admin role, if the user + // is not allowed to change role to admin + if ($role->name() === 'admin' && $isAdmin === false) { + continue; + } - $roles[] = [ - 'text' => $role->title(), - 'info' => $role->description() ?? t('role.description.placeholder'), - 'value' => $role->name() - ]; - } + $roles[] = [ + 'text' => $role->title(), + 'info' => $role->description() ?? I18n::translate('role.description.placeholder'), + 'value' => $role->name() + ]; + } - return array_merge([ - 'label' => t('role'), - 'type' => count($roles) <= 1 ? 'hidden' : 'radio', - 'options' => $roles - ], $props); - } + return array_merge([ + 'label' => I18n::translate('role'), + 'type' => count($roles) <= 1 ? 'hidden' : 'radio', + 'options' => $roles + ], $props); + } - /** - * @param array $props - * @return array - */ - public static function slug(array $props = []): array - { - return array_merge([ - 'label' => t('slug'), - 'type' => 'slug', - ], $props); - } + /** + * @param array $props + * @return array + */ + public static function slug(array $props = []): array + { + return array_merge([ + 'label' => I18n::translate('slug'), + 'type' => 'slug', + ], $props); + } - /** - * @param array $blueprints - * @param array $props - * @return array - */ - public static function template(?array $blueprints = [], ?array $props = []): array - { - $options = []; + /** + * @param array $blueprints + * @param array $props + * @return array + */ + public static function template(?array $blueprints = [], ?array $props = []): array + { + $options = []; - foreach ($blueprints as $blueprint) { - $options[] = [ - 'text' => $blueprint['title'] ?? $blueprint['text'] ?? null, - 'value' => $blueprint['name'] ?? $blueprint['value'] ?? null, - ]; - } + foreach ($blueprints as $blueprint) { + $options[] = [ + 'text' => $blueprint['title'] ?? $blueprint['text'] ?? null, + 'value' => $blueprint['name'] ?? $blueprint['value'] ?? null, + ]; + } - return array_merge([ - 'label' => t('template'), - 'type' => 'select', - 'empty' => false, - 'options' => $options, - 'icon' => 'template', - 'disabled' => count($options) <= 1 - ], $props); - } + return array_merge([ + 'label' => I18n::translate('template'), + 'type' => 'select', + 'empty' => false, + 'options' => $options, + 'icon' => 'template', + 'disabled' => count($options) <= 1 + ], $props); + } - /** - * @param array $props - * @return array - */ - public static function title(array $props = []): array - { - return array_merge([ - 'label' => t('title'), - 'type' => 'text', - 'icon' => 'title', - ], $props); - } + /** + * @param array $props + * @return array + */ + public static function title(array $props = []): array + { + return array_merge([ + 'label' => I18n::translate('title'), + 'type' => 'text', + 'icon' => 'title', + ], $props); + } - /** - * Panel translation select box - * - * @param array $props - * @return array - */ - public static function translation(array $props = []): array - { - $translations = []; - foreach (kirby()->translations() as $translation) { - $translations[] = [ - 'text' => $translation->name(), - 'value' => $translation->code() - ]; - } + /** + * Panel translation select box + * + * @param array $props + * @return array + */ + public static function translation(array $props = []): array + { + $translations = []; + foreach (App::instance()->translations() as $translation) { + $translations[] = [ + 'text' => $translation->name(), + 'value' => $translation->code() + ]; + } - return array_merge([ - 'label' => t('language'), - 'type' => 'select', - 'icon' => 'globe', - 'options' => $translations, - 'empty' => false - ], $props); - } + return array_merge([ + 'label' => I18n::translate('language'), + 'type' => 'select', + 'icon' => 'globe', + 'options' => $translations, + 'empty' => false + ], $props); + } - /** - * @param array $props - * @return array - */ - public static function username(array $props = []): array - { - return array_merge([ - 'icon' => 'user', - 'label' => t('name'), - 'type' => 'text', - ], $props); - } + /** + * @param array $props + * @return array + */ + public static function username(array $props = []): array + { + return array_merge([ + 'icon' => 'user', + 'label' => I18n::translate('name'), + 'type' => 'text', + ], $props); + } } diff --git a/kirby/src/Panel/File.php b/kirby/src/Panel/File.php index fc05641..c78fa7e 100644 --- a/kirby/src/Panel/File.php +++ b/kirby/src/Panel/File.php @@ -2,6 +2,7 @@ namespace Kirby\Panel; +use Kirby\Toolkit\I18n; use Throwable; /** @@ -16,452 +17,454 @@ use Throwable; */ class File extends Model { - /** - * Breadcrumb array - * - * @return array - */ - public function breadcrumb(): array - { - $breadcrumb = []; - $parent = $this->model->parent(); + /** + * @var \Kirby\Cms\File + */ + protected $model; - switch ($parent::CLASS_ALIAS) { - case 'user': - // The breadcrumb is not necessary - // on the account view - if ($parent->isLoggedIn() === false) { - $breadcrumb[] = [ - 'label' => $parent->username(), - 'link' => $parent->panel()->url(true) - ]; - } - break; - case 'page': - $breadcrumb = $this->model->parents()->flip()->values(fn ($parent) => [ - 'label' => $parent->title()->toString(), - 'link' => $parent->panel()->url(true), - ]); - } + /** + * Breadcrumb array + * + * @return array + */ + public function breadcrumb(): array + { + $breadcrumb = []; + $parent = $this->model->parent(); - // add the file - $breadcrumb[] = [ - 'label' => $this->model->filename(), - 'link' => $this->url(true), - ]; + switch ($parent::CLASS_ALIAS) { + case 'user': + // The breadcrumb is not necessary + // on the account view + if ($parent->isLoggedIn() === false) { + $breadcrumb[] = [ + 'label' => $parent->username(), + 'link' => $parent->panel()->url(true) + ]; + } + break; + case 'page': + $breadcrumb = $this->model->parents()->flip()->values(fn ($parent) => [ + 'label' => $parent->title()->toString(), + 'link' => $parent->panel()->url(true), + ]); + } - return $breadcrumb; - } + // add the file + $breadcrumb[] = [ + 'label' => $this->model->filename(), + 'link' => $this->url(true), + ]; - /** - * Provides a kirbytag or markdown - * tag for the file, which will be - * used in the panel, when the file - * gets dragged onto a textarea - * - * @internal - * @param string|null $type (`auto`|`kirbytext`|`markdown`) - * @param bool $absolute - * @return string - */ - public function dragText(string $type = null, bool $absolute = false): string - { - $type = $this->dragTextType($type); - $url = $absolute ? $this->model->id() : $this->model->filename(); + return $breadcrumb; + } - if ($dragTextFromCallback = $this->dragTextFromCallback($type, $url)) { - return $dragTextFromCallback; - } + /** + * Provides a kirbytag or markdown + * tag for the file, which will be + * used in the panel, when the file + * gets dragged onto a textarea + * + * @internal + * @param string|null $type (`auto`|`kirbytext`|`markdown`) + * @param bool $absolute + * @return string + */ + public function dragText(string $type = null, bool $absolute = false): string + { + $type = $this->dragTextType($type); + $url = $absolute ? $this->model->id() : $this->model->filename(); - if ($type === 'markdown') { - if ($this->model->type() === 'image') { - return '![' . $this->model->alt() . '](' . $url . ')'; - } + if ($dragTextFromCallback = $this->dragTextFromCallback($type, $url)) { + return $dragTextFromCallback; + } - return '[' . $this->model->filename() . '](' . $url . ')'; - } + if ($type === 'markdown') { + if ($this->model->type() === 'image') { + return '![' . $this->model->alt() . '](' . $url . ')'; + } - if ($this->model->type() === 'image') { - return '(image: ' . $url . ')'; - } - if ($this->model->type() === 'video') { - return '(video: ' . $url . ')'; - } + return '[' . $this->model->filename() . '](' . $url . ')'; + } - return '(file: ' . $url . ')'; - } + if ($this->model->type() === 'image') { + return '(image: ' . $url . ')'; + } + if ($this->model->type() === 'video') { + return '(video: ' . $url . ')'; + } - /** - * Provides options for the file dropdown - * - * @param array $options - * @return array - */ - public function dropdown(array $options = []): array - { - $defaults = [ - 'view' => get('view'), - 'update' => get('update'), - 'delete' => get('delete') - ]; + return '(file: ' . $url . ')'; + } - $options = array_merge($defaults, $options); - $file = $this->model; - $permissions = $this->options(['preview']); - $view = $options['view'] ?? 'view'; - $url = $this->url(true); - $result = []; + /** + * Provides options for the file dropdown + * + * @param array $options + * @return array + */ + public function dropdown(array $options = []): array + { + $file = $this->model; - if ($view === 'list') { - $result[] = [ - 'link' => $file->previewUrl(), - 'target' => '_blank', - 'icon' => 'open', - 'text' => t('open') - ]; - $result[] = '-'; - } + $defaults = $file->kirby()->request()->get(['view', 'update', 'delete']); + $options = array_merge($defaults, $options); - $result[] = [ - 'dialog' => $url . '/changeName', - 'icon' => 'title', - 'text' => t('rename'), - 'disabled' => $this->isDisabledDropdownOption('changeName', $options, $permissions) - ]; + $permissions = $this->options(['preview']); + $view = $options['view'] ?? 'view'; + $url = $this->url(true); + $result = []; - $result[] = [ - 'click' => 'replace', - 'icon' => 'upload', - 'text' => t('replace'), - 'disabled' => $this->isDisabledDropdownOption('replace', $options, $permissions) - ]; + if ($view === 'list') { + $result[] = [ + 'link' => $file->previewUrl(), + 'target' => '_blank', + 'icon' => 'open', + 'text' => I18n::translate('open') + ]; + $result[] = '-'; + } - if ($view === 'list') { - $result[] = '-'; - $result[] = [ - 'dialog' => $url . '/changeSort', - 'icon' => 'sort', - 'text' => t('file.sort'), - 'disabled' => $this->isDisabledDropdownOption('update', $options, $permissions) - ]; - } + $result[] = [ + 'dialog' => $url . '/changeName', + 'icon' => 'title', + 'text' => I18n::translate('rename'), + 'disabled' => $this->isDisabledDropdownOption('changeName', $options, $permissions) + ]; - $result[] = '-'; - $result[] = [ - 'dialog' => $url . '/delete', - 'icon' => 'trash', - 'text' => t('delete'), - 'disabled' => $this->isDisabledDropdownOption('delete', $options, $permissions) - ]; + $result[] = [ + 'click' => 'replace', + 'icon' => 'upload', + 'text' => I18n::translate('replace'), + 'disabled' => $this->isDisabledDropdownOption('replace', $options, $permissions) + ]; - return $result; - } + if ($view === 'list') { + $result[] = '-'; + $result[] = [ + 'dialog' => $url . '/changeSort', + 'icon' => 'sort', + 'text' => I18n::translate('file.sort'), + 'disabled' => $this->isDisabledDropdownOption('update', $options, $permissions) + ]; + } - /** - * Returns the setup for a dropdown option - * which is used in the changes dropdown - * for example. - * - * @return array - */ - public function dropdownOption(): array - { - return [ - 'icon' => 'image', - 'text' => $this->model->filename(), - ] + parent::dropdownOption(); - } + $result[] = '-'; + $result[] = [ + 'dialog' => $url . '/delete', + 'icon' => 'trash', + 'text' => I18n::translate('delete'), + 'disabled' => $this->isDisabledDropdownOption('delete', $options, $permissions) + ]; - /** - * Returns the Panel icon color - * - * @return string - */ - protected function imageColor(): string - { - $types = [ - 'image' => 'orange-400', - 'video' => 'yellow-400', - 'document' => 'red-400', - 'audio' => 'aqua-400', - 'code' => 'blue-400', - 'archive' => 'white' - ]; + return $result; + } - $extensions = [ - 'indd' => 'purple-400', - 'xls' => 'green-400', - 'xlsx' => 'green-400', - 'csv' => 'green-400', - 'docx' => 'blue-400', - 'doc' => 'blue-400', - 'rtf' => 'blue-400' - ]; + /** + * Returns the setup for a dropdown option + * which is used in the changes dropdown + * for example. + * + * @return array + */ + public function dropdownOption(): array + { + return [ + 'icon' => 'image', + 'text' => $this->model->filename(), + ] + parent::dropdownOption(); + } - return $extensions[$this->model->extension()] ?? - $types[$this->model->type()] ?? - parent::imageDefaults()['icon']; - } + /** + * Returns the Panel icon color + * + * @return string + */ + protected function imageColor(): string + { + $types = [ + 'image' => 'orange-400', + 'video' => 'yellow-400', + 'document' => 'red-400', + 'audio' => 'aqua-400', + 'code' => 'blue-400', + 'archive' => 'gray-500' + ]; - /** - * Default settings for the file's Panel image - * - * @return array - */ - protected function imageDefaults(): array - { - return array_merge(parent::imageDefaults(), [ - 'color' => $this->imageColor(), - 'icon' => $this->imageIcon(), - ]); - } + $extensions = [ + 'indd' => 'purple-400', + 'xls' => 'green-400', + 'xlsx' => 'green-400', + 'csv' => 'green-400', + 'docx' => 'blue-400', + 'doc' => 'blue-400', + 'rtf' => 'blue-400' + ]; - /** - * Returns the Panel icon type - * - * @return string - */ - protected function imageIcon(): string - { - $types = [ - 'image' => 'file-image', - 'video' => 'file-video', - 'document' => 'file-document', - 'audio' => 'file-audio', - 'code' => 'file-code', - 'archive' => 'file-zip' - ]; + return $extensions[$this->model->extension()] ?? + $types[$this->model->type()] ?? + parent::imageDefaults()['color']; + } - $extensions = [ - 'xls' => 'file-spreadsheet', - 'xlsx' => 'file-spreadsheet', - 'csv' => 'file-spreadsheet', - 'docx' => 'file-word', - 'doc' => 'file-word', - 'rtf' => 'file-word', - 'mdown' => 'file-text', - 'md' => 'file-text' - ]; + /** + * Default settings for the file's Panel image + * + * @return array + */ + protected function imageDefaults(): array + { + return array_merge(parent::imageDefaults(), [ + 'color' => $this->imageColor(), + 'icon' => $this->imageIcon(), + ]); + } - return $extensions[$this->model->extension()] ?? - $types[$this->model->type()] ?? - parent::imageDefaults()['color']; - } + /** + * Returns the Panel icon type + * + * @return string + */ + protected function imageIcon(): string + { + $types = [ + 'image' => 'image', + 'video' => 'video', + 'document' => 'document', + 'audio' => 'audio', + 'code' => 'code', + 'archive' => 'archive' + ]; - /** - * Returns the image file object based on provided query - * - * @internal - * @param string|null $query - * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null - */ - protected function imageSource(string $query = null) - { - if ($query === null && $this->model->isViewable()) { - return $this->model; - } + $extensions = [ + 'xls' => 'table', + 'xlsx' => 'table', + 'csv' => 'table', + 'docx' => 'pen', + 'doc' => 'pen', + 'rtf' => 'pen', + 'mdown' => 'markdown', + 'md' => 'markdown' + ]; - return parent::imageSource($query); - } + return $extensions[$this->model->extension()] ?? + $types[$this->model->type()] ?? + 'file'; + } - /** - * Returns an array of all actions - * that can be performed in the Panel - * - * @param array $unlock An array of options that will be force-unlocked - * @return array - */ - public function options(array $unlock = []): array - { - $options = parent::options($unlock); + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null + */ + protected function imageSource(string $query = null) + { + if ($query === null && $this->model->isViewable()) { + return $this->model; + } - try { - // check if the file type is allowed at all, - // otherwise it cannot be replaced - $this->model->match($this->model->blueprint()->accept()); - } catch (Throwable $e) { - $options['replace'] = false; - } + return parent::imageSource($query); + } - return $options; - } + /** + * Returns an array of all actions + * that can be performed in the Panel + * + * @param array $unlock An array of options that will be force-unlocked + * @return array + */ + public function options(array $unlock = []): array + { + $options = parent::options($unlock); - /** - * Returns the full path without leading slash - * - * @return string - */ - public function path(): string - { - return 'files/' . $this->model->filename(); - } + try { + // check if the file type is allowed at all, + // otherwise it cannot be replaced + $this->model->match($this->model->blueprint()->accept()); + } catch (Throwable $e) { + $options['replace'] = false; + } - /** - * Prepares the response data for file pickers - * and file fields - * - * @param array|null $params - * @return array - */ - public function pickerData(array $params = []): array - { - $id = $this->model->id(); - $name = $this->model->filename(); + return $options; + } - if (empty($params['model']) === false) { - $parent = $this->model->parent(); - $uuid = $parent === $params['model'] ? $name : $id; - $absolute = $parent !== $params['model']; - } + /** + * Returns the full path without leading slash + * + * @return string + */ + public function path(): string + { + return 'files/' . $this->model->filename(); + } - $params['text'] ??= '{{ file.filename }}'; + /** + * Prepares the response data for file pickers + * and file fields + * + * @param array|null $params + * @return array + */ + public function pickerData(array $params = []): array + { + $id = $this->model->id(); + $name = $this->model->filename(); - return array_merge(parent::pickerData($params), [ - 'filename' => $name, - 'dragText' => $this->dragText('auto', $absolute ?? false), - 'type' => $this->model->type(), - 'url' => $this->model->url(), - 'uuid' => $uuid ?? $id, - ]); - } + if (empty($params['model']) === false) { + $parent = $this->model->parent(); + $uuid = $parent === $params['model'] ? $name : $id; + $absolute = $parent !== $params['model']; + } - /** - * Returns the data array for the - * view's component props - * - * @internal - * - * @return array - */ - public function props(): array - { - $file = $this->model; - $dimensions = $file->dimensions(); - $siblings = $file->templateSiblings()->sortBy( - 'sort', - 'asc', - 'filename', - 'asc' - ); + $params['text'] ??= '{{ file.filename }}'; + + return array_merge(parent::pickerData($params), [ + 'filename' => $name, + 'dragText' => $this->dragText('auto', $absolute ?? false), + 'type' => $this->model->type(), + 'url' => $this->model->url(), + 'uuid' => $uuid ?? $id, + ]); + } + + /** + * Returns the data array for the + * view's component props + * + * @internal + * + * @return array + */ + public function props(): array + { + $file = $this->model; + $dimensions = $file->dimensions(); + $siblings = $file->templateSiblings()->sortBy( + 'sort', + 'asc', + 'filename', + 'asc' + ); - return array_merge( - parent::props(), - $this->prevNext(), - [ - 'blueprint' => $this->model->template() ?? 'default', - 'model' => [ - 'content' => $this->content(), - 'dimensions' => $dimensions->toArray(), - 'extension' => $file->extension(), - 'filename' => $file->filename(), - 'link' => $this->url(true), - 'mime' => $file->mime(), - 'niceSize' => $file->niceSize(), - 'id' => $id = $file->id(), - 'parent' => $file->parent()->panel()->path(), - 'template' => $file->template(), - 'type' => $file->type(), - 'url' => $file->url(), - ], - 'preview' => [ - 'image' => $this->image([ - 'back' => 'transparent', - 'ratio' => '1/1' - ], 'cards'), - 'url' => $url = $file->previewUrl(), - 'details' => [ - [ - 'title' => t('template'), - 'text' => $file->template() ?? '—' - ], - [ - 'title' => t('mime'), - 'text' => $file->mime() - ], - [ - 'title' => t('url'), - 'text' => $id, - 'link' => $url - ], - [ - 'title' => t('size'), - 'text' => $file->niceSize() - ], - [ - 'title' => t('dimensions'), - 'text' => $file->type() === 'image' ? $file->dimensions() . ' ' . t('pixel') : '—' - ], - [ - 'title' => t('orientation'), - 'text' => $file->type() === 'image' ? t('orientation.' . $dimensions->orientation()) : '—' - ], - ] - ] - ] - ); - } + return array_merge( + parent::props(), + $this->prevNext(), + [ + 'blueprint' => $this->model->template() ?? 'default', + 'model' => [ + 'content' => $this->content(), + 'dimensions' => $dimensions->toArray(), + 'extension' => $file->extension(), + 'filename' => $file->filename(), + 'link' => $this->url(true), + 'mime' => $file->mime(), + 'niceSize' => $file->niceSize(), + 'id' => $id = $file->id(), + 'parent' => $file->parent()->panel()->path(), + 'template' => $file->template(), + 'type' => $file->type(), + 'url' => $file->url(), + ], + 'preview' => [ + 'image' => $this->image([ + 'back' => 'transparent', + 'ratio' => '1/1' + ], 'cards'), + 'url' => $url = $file->previewUrl(), + 'details' => [ + [ + 'title' => I18n::translate('template'), + 'text' => $file->template() ?? '—' + ], + [ + 'title' => I18n::translate('mime'), + 'text' => $file->mime() + ], + [ + 'title' => I18n::translate('url'), + 'text' => $id, + 'link' => $url + ], + [ + 'title' => I18n::translate('size'), + 'text' => $file->niceSize() + ], + [ + 'title' => I18n::translate('dimensions'), + 'text' => $file->type() === 'image' ? $file->dimensions() . ' ' . I18n::translate('pixel') : '—' + ], + [ + 'title' => I18n::translate('orientation'), + 'text' => $file->type() === 'image' ? I18n::translate('orientation.' . $dimensions->orientation()) : '—' + ], + ] + ] + ] + ); + } - /** - * Returns navigation array with - * previous and next file - * - * @internal - * - * @return array - */ - public function prevNext(): array - { - $file = $this->model; - $siblings = $file->templateSiblings()->sortBy( - 'sort', - 'asc', - 'filename', - 'asc' - ); + /** + * Returns navigation array with + * previous and next file + * + * @internal + * + * @return array + */ + public function prevNext(): array + { + $file = $this->model; + $siblings = $file->templateSiblings()->sortBy( + 'sort', + 'asc', + 'filename', + 'asc' + ); - return [ - 'next' => function () use ($file, $siblings): ?array { - $next = $siblings->nth($siblings->indexOf($file) + 1); - return $next ? $next->panel()->toLink('filename') : null; - }, - 'prev' => function () use ($file, $siblings): ?array { - $prev = $siblings->nth($siblings->indexOf($file) - 1); - return $prev ? $prev->panel()->toLink('filename') : null; - } - ]; - } - /** - * Returns the url to the editing view - * in the panel - * - * @param bool $relative - * @return string - */ - public function url(bool $relative = false): string - { - $parent = $this->model->parent()->panel()->url($relative); - return $parent . '/' . $this->path(); - } + return [ + 'next' => function () use ($file, $siblings): ?array { + $next = $siblings->nth($siblings->indexOf($file) + 1); + return $this->toPrevNextLink($next, 'filename'); + }, + 'prev' => function () use ($file, $siblings): ?array { + $prev = $siblings->nth($siblings->indexOf($file) - 1); + return $this->toPrevNextLink($prev, 'filename'); + } + ]; + } + /** + * Returns the url to the editing view + * in the panel + * + * @param bool $relative + * @return string + */ + public function url(bool $relative = false): string + { + $parent = $this->model->parent()->panel()->url($relative); + return $parent . '/' . $this->path(); + } - /** - * Returns the data array for - * this model's Panel view - * - * @internal - * - * @return array - */ - public function view(): array - { - $file = $this->model; + /** + * Returns the data array for + * this model's Panel view + * + * @internal + * + * @return array + */ + public function view(): array + { + $file = $this->model; - return [ - 'breadcrumb' => fn (): array => $file->panel()->breadcrumb(), - 'component' => 'k-file-view', - 'props' => $this->props(), - 'search' => 'files', - 'title' => $file->filename(), - ]; - } + return [ + 'breadcrumb' => fn (): array => $file->panel()->breadcrumb(), + 'component' => 'k-file-view', + 'props' => $this->props(), + 'search' => 'files', + 'title' => $file->filename(), + ]; + } } diff --git a/kirby/src/Panel/Home.php b/kirby/src/Panel/Home.php index df9d6f6..434673c 100644 --- a/kirby/src/Panel/Home.php +++ b/kirby/src/Panel/Home.php @@ -2,9 +2,11 @@ namespace Kirby\Panel; +use Kirby\Cms\App; use Kirby\Cms\User; use Kirby\Exception\InvalidArgumentException; use Kirby\Exception\NotFoundException; +use Kirby\Http\Router; use Kirby\Http\Uri; use Kirby\Toolkit\Str; use Throwable; @@ -29,233 +31,234 @@ use Throwable; */ class Home { - /** - * Returns an alternative URL if access - * to the first choice is blocked. - * - * It will go through the entire menu and - * take the first area which is not disabled - * or locked in other ways - * - * @param \Kirby\Cms\User $user - * @return string - */ - public static function alternative(User $user): string - { - $permissions = $user->role()->permissions(); + /** + * Returns an alternative URL if access + * to the first choice is blocked. + * + * It will go through the entire menu and + * take the first area which is not disabled + * or locked in other ways + * + * @param \Kirby\Cms\User $user + * @return string + */ + public static function alternative(User $user): string + { + $permissions = $user->role()->permissions(); - // no access to the panel? The only good alternative is the main url - if ($permissions->for('access', 'panel') === false) { - return site()->url(); - } + // no access to the panel? The only good alternative is the main url + if ($permissions->for('access', 'panel') === false) { + return App::instance()->site()->url(); + } - // needed to create a proper menu - $areas = Panel::areas(); - $menu = View::menu($areas, $permissions->toArray()); + // needed to create a proper menu + $areas = Panel::areas(); + $menu = View::menu($areas, $permissions->toArray()); - // go through the menu and search for the first - // available view we can go to - foreach ($menu as $menuItem) { - // skip separators - if ($menuItem === '-') { - continue; - } + // go through the menu and search for the first + // available view we can go to + foreach ($menu as $menuItem) { + // skip separators + if ($menuItem === '-') { + continue; + } - // skip disabled items - if (($menuItem['disabled'] ?? false) === true) { - continue; - } + // skip disabled items + if (($menuItem['disabled'] ?? false) === true) { + continue; + } - // skip the logout button - if ($menuItem['id'] === 'logout') { - continue; - } + // skip the logout button + if ($menuItem['id'] === 'logout') { + continue; + } - return Panel::url($menuItem['link']); - } + return Panel::url($menuItem['link']); + } - throw new NotFoundException('There’s no available Panel page to redirect to'); - } + throw new NotFoundException('There’s no available Panel page to redirect to'); + } - /** - * Checks if the user has access to the given - * panel path. This is quite tricky, because we - * need to call a trimmed down router to check - * for available routes and their firewall status. - * - * @param \Kirby\Cms\User - * @param string $path - * @return bool - */ - public static function hasAccess(User $user, string $path): bool - { - $areas = Panel::areas(); - $routes = Panel::routes($areas); + /** + * Checks if the user has access to the given + * panel path. This is quite tricky, because we + * need to call a trimmed down router to check + * for available routes and their firewall status. + * + * @param \Kirby\Cms\User + * @param string $path + * @return bool + */ + public static function hasAccess(User $user, string $path): bool + { + $areas = Panel::areas(); + $routes = Panel::routes($areas); - // Remove fallback routes. Otherwise a route - // would be found even if the view does - // not exist at all. - foreach ($routes as $index => $route) { - if ($route['pattern'] === '(:all)') { - unset($routes[$index]); - } - } + // Remove fallback routes. Otherwise a route + // would be found even if the view does + // not exist at all. + foreach ($routes as $index => $route) { + if ($route['pattern'] === '(:all)') { + unset($routes[$index]); + } + } - // create a dummy router to check if we can access this route at all - try { - return router($path, 'GET', $routes, function ($route) use ($user) { - $auth = $route->attributes()['auth'] ?? true; - $areaId = $route->attributes()['area'] ?? null; - $type = $route->attributes()['type'] ?? 'view'; + // create a dummy router to check if we can access this route at all + try { + return Router::execute($path, 'GET', $routes, function ($route) use ($user) { + $auth = $route->attributes()['auth'] ?? true; + $areaId = $route->attributes()['area'] ?? null; + $type = $route->attributes()['type'] ?? 'view'; - // only allow redirects to views - if ($type !== 'view') { - return false; - } + // only allow redirects to views + if ($type !== 'view') { + return false; + } - // if auth is not required the redirect is allowed - if ($auth === false) { - return true; - } + // if auth is not required the redirect is allowed + if ($auth === false) { + return true; + } - // check the firewall - return Panel::hasAccess($user, $areaId); - }); - } catch (Throwable $e) { - return false; - } - } + // check the firewall + return Panel::hasAccess($user, $areaId); + }); + } catch (Throwable $e) { + return false; + } + } - /** - * Checks if the given Uri has the same domain - * as the index URL of the Kirby installation. - * This is used to block external URLs to third-party - * domains as redirect options. - * - * @param \Kirby\Http\Uri $uri - * @return bool - */ - public static function hasValidDomain(Uri $uri): bool - { - return $uri->domain() === (new Uri(site()->url()))->domain(); - } + /** + * Checks if the given Uri has the same domain + * as the index URL of the Kirby installation. + * This is used to block external URLs to third-party + * domains as redirect options. + * + * @param \Kirby\Http\Uri $uri + * @return bool + */ + public static function hasValidDomain(Uri $uri): bool + { + $rootUrl = App::instance()->site()->url(); + return $uri->domain() === (new Uri($rootUrl))->domain(); + } - /** - * Checks if the given URL is a Panel Url. - * - * @param string $url - * @return bool - */ - public static function isPanelUrl(string $url): bool - { - return Str::startsWith($url, kirby()->url('panel')); - } + /** + * Checks if the given URL is a Panel Url. + * + * @param string $url + * @return bool + */ + public static function isPanelUrl(string $url): bool + { + return Str::startsWith($url, App::instance()->url('panel')); + } - /** - * Returns the path after /panel/ which can then - * be used in the router or to find a matching view - * - * @param string $url - * @return string|null - */ - public static function panelPath(string $url): ?string - { - $after = Str::after($url, kirby()->url('panel')); - return trim($after, '/'); - } + /** + * Returns the path after /panel/ which can then + * be used in the router or to find a matching view + * + * @param string $url + * @return string|null + */ + public static function panelPath(string $url): ?string + { + $after = Str::after($url, App::instance()->url('panel')); + return trim($after, '/'); + } - /** - * Returns the Url that has been stored in the session - * before the last logout. We take this Url if possible - * to redirect the user back to the last point where they - * left before they got logged out. - * - * @return string|null - */ - public static function remembered(): ?string - { - // check for a stored path after login - $remembered = kirby()->session()->pull('panel.path'); + /** + * Returns the Url that has been stored in the session + * before the last logout. We take this Url if possible + * to redirect the user back to the last point where they + * left before they got logged out. + * + * @return string|null + */ + public static function remembered(): ?string + { + // check for a stored path after login + $remembered = App::instance()->session()->pull('panel.path'); - // convert the result to an absolute URL if available - return $remembered ? Panel::url($remembered) : null; - } + // convert the result to an absolute URL if available + return $remembered ? Panel::url($remembered) : null; + } - /** - * Tries to find the best possible Url to redirect - * the user to after the login. - * - * When the user got logged out, we try to send them back - * to the point where they left. - * - * If they have a custom redirect Url defined in their blueprint - * via the `home` option, we send them there if no Url is stored - * in the session. - * - * If none of the options above find any result, we try to send - * them to the site view. - * - * Before the redirect happens, the final Url is sanitized, the query - * and params are removed to avoid any attacks and the domain is compared - * to avoid redirects to external Urls. - * - * Afterwards, we also check for permissions before the redirect happens - * to avoid redirects to inaccessible Panel views. In such a case - * the next best accessible view is picked from the menu. - * - * @return string - */ - public static function url(): string - { - $user = kirby()->user(); + /** + * Tries to find the best possible Url to redirect + * the user to after the login. + * + * When the user got logged out, we try to send them back + * to the point where they left. + * + * If they have a custom redirect Url defined in their blueprint + * via the `home` option, we send them there if no Url is stored + * in the session. + * + * If none of the options above find any result, we try to send + * them to the site view. + * + * Before the redirect happens, the final Url is sanitized, the query + * and params are removed to avoid any attacks and the domain is compared + * to avoid redirects to external Urls. + * + * Afterwards, we also check for permissions before the redirect happens + * to avoid redirects to inaccessible Panel views. In such a case + * the next best accessible view is picked from the menu. + * + * @return string + */ + public static function url(): string + { + $user = App::instance()->user(); - // if there's no authenticated user, all internal - // redirects will be blocked and the user is redirected - // to the login instead - if (!$user) { - return Panel::url('login'); - } + // if there's no authenticated user, all internal + // redirects will be blocked and the user is redirected + // to the login instead + if (!$user) { + return Panel::url('login'); + } - // get the last visited url from the session or the custom home - $url = static::remembered() ?? $user->panel()->home(); + // get the last visited url from the session or the custom home + $url = static::remembered() ?? $user->panel()->home(); - // inspect the given URL - $uri = new Uri($url); + // inspect the given URL + $uri = new Uri($url); - // compare domains to avoid external redirects - if (static::hasValidDomain($uri) !== true) { - throw new InvalidArgumentException('External URLs are not allowed for Panel redirects'); - } + // compare domains to avoid external redirects + if (static::hasValidDomain($uri) !== true) { + throw new InvalidArgumentException('External URLs are not allowed for Panel redirects'); + } - // remove all params to avoid - // possible attack vectors - $uri->params = ''; - $uri->query = ''; + // remove all params to avoid + // possible attack vectors + $uri->params = ''; + $uri->query = ''; - // get a clean version of the URL - $url = $uri->toString(); + // get a clean version of the URL + $url = $uri->toString(); - // Don't further inspect URLs outside of the Panel - if (static::isPanelUrl($url) === false) { - return $url; - } + // Don't further inspect URLs outside of the Panel + if (static::isPanelUrl($url) === false) { + return $url; + } - // get the plain panel path - $path = static::panelPath($url); + // get the plain panel path + $path = static::panelPath($url); - // a redirect to login, logout or installation - // views would lead to an infinite redirect loop - if (in_array($path, ['', 'login', 'logout', 'installation'], true) === true) { - $path = 'site'; - } + // a redirect to login, logout or installation + // views would lead to an infinite redirect loop + if (in_array($path, ['', 'login', 'logout', 'installation'], true) === true) { + $path = 'site'; + } - // Check if the user can access the URL - if (static::hasAccess($user, $path) === true) { - return Panel::url($path); - } + // Check if the user can access the URL + if (static::hasAccess($user, $path) === true) { + return Panel::url($path); + } - // Try to find an alternative - return static::alternative($user); - } + // Try to find an alternative + return static::alternative($user); + } } diff --git a/kirby/src/Panel/Json.php b/kirby/src/Panel/Json.php index e1c9c8d..926046e 100644 --- a/kirby/src/Panel/Json.php +++ b/kirby/src/Panel/Json.php @@ -17,61 +17,61 @@ namespace Kirby\Panel; */ abstract class Json { - protected static $key = '$response'; + protected static $key = '$response'; - /** - * Renders the error response with the provided message - * - * @param string $message - * @param int $code - * @return array - */ - public static function error(string $message, int $code = 404) - { - return [ - 'code' => $code, - 'error' => $message - ]; - } + /** + * Renders the error response with the provided message + * + * @param string $message + * @param int $code + * @return array + */ + public static function error(string $message, int $code = 404) + { + return [ + 'code' => $code, + 'error' => $message + ]; + } - /** - * Prepares the JSON response for the Panel - * - * @param mixed $data - * @param array $options - * @return mixed - */ - public static function response($data, array $options = []) - { - // handle redirects - if (is_a($data, 'Kirby\Panel\Redirect') === true) { - $data = [ - 'redirect' => $data->location(), - 'code' => $data->code() - ]; + /** + * Prepares the JSON response for the Panel + * + * @param mixed $data + * @param array $options + * @return mixed + */ + public static function response($data, array $options = []) + { + // handle redirects + if (is_a($data, 'Kirby\Panel\Redirect') === true) { + $data = [ + 'redirect' => $data->location(), + 'code' => $data->code() + ]; - // handle Kirby exceptions - } elseif (is_a($data, 'Kirby\Exception\Exception') === true) { - $data = static::error($data->getMessage(), $data->getHttpCode()); + // handle Kirby exceptions + } elseif (is_a($data, 'Kirby\Exception\Exception') === true) { + $data = static::error($data->getMessage(), $data->getHttpCode()); - // handle exceptions - } elseif (is_a($data, 'Throwable') === true) { - $data = static::error($data->getMessage(), 500); + // handle exceptions + } elseif (is_a($data, 'Throwable') === true) { + $data = static::error($data->getMessage(), 500); - // only expect arrays from here on - } elseif (is_array($data) === false) { - $data = static::error('Invalid response', 500); - } + // only expect arrays from here on + } elseif (is_array($data) === false) { + $data = static::error('Invalid response', 500); + } - if (empty($data) === true) { - $data = static::error('The response is empty', 404); - } + if (empty($data) === true) { + $data = static::error('The response is empty', 404); + } - // always inject the response code - $data['code'] ??= 200; - $data['path'] = $options['path'] ?? null; - $data['referrer'] = Panel::referrer(); + // always inject the response code + $data['code'] ??= 200; + $data['path'] = $options['path'] ?? null; + $data['referrer'] = Panel::referrer(); - return Panel::json([static::$key => $data], $data['code']); - } + return Panel::json([static::$key => $data], $data['code']); + } } diff --git a/kirby/src/Panel/Model.php b/kirby/src/Panel/Model.php index d845449..6fc7675 100644 --- a/kirby/src/Panel/Model.php +++ b/kirby/src/Panel/Model.php @@ -3,6 +3,7 @@ namespace Kirby\Panel; use Kirby\Form\Form; +use Kirby\Http\Uri; use Kirby\Toolkit\A; /** @@ -17,401 +18,433 @@ use Kirby\Toolkit\A; */ abstract class Model { - /** - * @var \Kirby\Cms\ModelWithContent - */ - protected $model; + /** + * @var \Kirby\Cms\ModelWithContent + */ + protected $model; - /** - * @param \Kirby\Cms\ModelWithContent $model - */ - public function __construct($model) - { - $this->model = $model; - } + /** + * @param \Kirby\Cms\ModelWithContent $model + */ + public function __construct($model) + { + $this->model = $model; + } - /** - * Get the content values for the model - * - * @return array - */ - public function content(): array - { - return Form::for($this->model)->values(); - } + /** + * Get the content values for the model + * + * @return array + */ + public function content(): array + { + return Form::for($this->model)->values(); + } - /** - * Returns the drag text from a custom callback - * if the callback is defined in the config - * @internal - * - * @param string $type markdown or kirbytext - * @param mixed ...$args - * @return string|null - */ - public function dragTextFromCallback(string $type, ...$args): ?string - { - $option = 'panel.' . $type . '.' . $this->model::CLASS_ALIAS . 'DragText'; - $callback = option($option); + /** + * Returns the drag text from a custom callback + * if the callback is defined in the config + * @internal + * + * @param string $type markdown or kirbytext + * @param mixed ...$args + * @return string|null + */ + public function dragTextFromCallback(string $type, ...$args): ?string + { + $option = 'panel.' . $type . '.' . $this->model::CLASS_ALIAS . 'DragText'; + $callback = $this->model->kirby()->option($option); - if ( - empty($callback) === false && - is_a($callback, 'Closure') === true && - ($dragText = $callback($this->model, ...$args)) !== null - ) { - return $dragText; - } + if ( + empty($callback) === false && + is_a($callback, 'Closure') === true && + ($dragText = $callback($this->model, ...$args)) !== null + ) { + return $dragText; + } - return null; - } + return null; + } - /** - * Returns the correct drag text type - * depending on the given type or the - * configuration - * - * @internal - * - * @param string|null $type (`auto`|`kirbytext`|`markdown`) - * @return string - */ - public function dragTextType(string $type = null): string - { - $type ??= 'auto'; + /** + * Returns the correct drag text type + * depending on the given type or the + * configuration + * + * @internal + * + * @param string|null $type (`auto`|`kirbytext`|`markdown`) + * @return string + */ + public function dragTextType(string $type = null): string + { + $type ??= 'auto'; - if ($type === 'auto') { - $type = option('panel.kirbytext', true) ? 'kirbytext' : 'markdown'; - } + if ($type === 'auto') { + $kirby = $this->model->kirby(); + $type = $kirby->option('panel.kirbytext', true) ? 'kirbytext' : 'markdown'; + } - return $type === 'markdown' ? 'markdown' : 'kirbytext'; - } + return $type === 'markdown' ? 'markdown' : 'kirbytext'; + } - /** - * Returns the setup for a dropdown option - * which is used in the changes dropdown - * for example. - * - * @return array - */ - public function dropdownOption(): array - { - return [ - 'icon' => 'page', - 'link' => $this->url(), - 'text' => $this->model->id(), - ]; - } + /** + * Returns the setup for a dropdown option + * which is used in the changes dropdown + * for example. + * + * @return array + */ + public function dropdownOption(): array + { + return [ + 'icon' => 'page', + 'link' => $this->url(), + 'text' => $this->model->id(), + ]; + } - /** - * Returns the Panel image definition - * - * @internal - * - * @param string|array|false|null $settings - * @return array|null - */ - public function image($settings = [], string $layout = 'list'): ?array - { - // completely switched off - if ($settings === false) { - return null; - } + /** + * Returns the Panel image definition + * + * @internal + * + * @param string|array|false|null $settings + * @return array|null + */ + public function image($settings = [], string $layout = 'list'): ?array + { + // completely switched off + if ($settings === false) { + return null; + } - // skip image thumbnail if option - // is explicitly set to show the icon - if ($settings === 'icon') { - $settings = [ - 'query' => false - ]; - } elseif (is_string($settings) === true) { - // convert string settings to proper array - $settings = [ - 'query' => $settings - ]; - } + // skip image thumbnail if option + // is explicitly set to show the icon + if ($settings === 'icon') { + $settings = [ + 'query' => false + ]; + } elseif (is_string($settings) === true) { + // convert string settings to proper array + $settings = [ + 'query' => $settings + ]; + } - // merge with defaults and blueprint option - $settings = array_merge( - $this->imageDefaults(), - $settings ?? [], - $this->model->blueprint()->image() ?? [], - ); + // merge with defaults and blueprint option + $settings = array_merge( + $this->imageDefaults(), + $settings ?? [], + $this->model->blueprint()->image() ?? [], + ); - if ($image = $this->imageSource($settings['query'] ?? null)) { - // main url - $settings['url'] = $image->url(); + if ($image = $this->imageSource($settings['query'] ?? null)) { + // main url + $settings['url'] = $image->url(); - // only create srcsets for resizable files - if ($image->isResizable() === true) { - $settings['src'] = static::imagePlaceholder(); + // only create srcsets for resizable files + if ($image->isResizable() === true) { + $settings['src'] = static::imagePlaceholder(); - switch ($layout) { - case 'cards': - $sizes = [352, 864, 1408]; - break; - case 'cardlets': - $sizes = [96, 192]; - break; - case 'list': - default: - $sizes = [38, 76]; - break; - } + switch ($layout) { + case 'cards': + $sizes = [352, 864, 1408]; + break; + case 'cardlets': + $sizes = [96, 192]; + break; + case 'list': + default: + $sizes = [38, 76]; + break; + } - if (($settings['cover'] ?? false) === false || $layout === 'cards') { - $settings['srcset'] = $image->srcset($sizes); - } else { - $settings['srcset'] = $image->srcset([ - '1x' => [ - 'width' => $sizes[0], - 'height' => $sizes[0], - 'crop' => 'center' - ], - '2x' => [ - 'width' => $sizes[1], - 'height' => $sizes[1], - 'crop' => 'center' - ] - ]); - } - } elseif ($image->isViewable() === true) { - $settings['src'] = $image->url(); - } - } + if (($settings['cover'] ?? false) === false || $layout === 'cards') { + $settings['srcset'] = $image->srcset($sizes); + } else { + $settings['srcset'] = $image->srcset([ + '1x' => [ + 'width' => $sizes[0], + 'height' => $sizes[0], + 'crop' => 'center' + ], + '2x' => [ + 'width' => $sizes[1], + 'height' => $sizes[1], + 'crop' => 'center' + ] + ]); + } + } elseif ($image->isViewable() === true) { + $settings['src'] = $image->url(); + } + } - if (isset($settings['query']) === true) { - unset($settings['query']); - } + if (isset($settings['query']) === true) { + unset($settings['query']); + } - // resolve remaining options defined as query - return A::map($settings, function ($option) { - if (is_string($option) === false) { - return $option; - } + // resolve remaining options defined as query + return A::map($settings, function ($option) { + if (is_string($option) === false) { + return $option; + } - return $this->model->toString($option); - }); - } + return $this->model->toString($option); + }); + } - /** - * Default settings for Panel image - * - * @return array - */ - protected function imageDefaults(): array - { - return [ - 'back' => 'pattern', - 'color' => 'gray-500', - 'cover' => false, - 'icon' => 'page', - 'ratio' => '3/2', - ]; - } + /** + * Default settings for Panel image + * + * @return array + */ + protected function imageDefaults(): array + { + return [ + 'back' => 'pattern', + 'color' => 'gray-500', + 'cover' => false, + 'icon' => 'page', + 'ratio' => '3/2', + ]; + } - /** - * Data URI placeholder string for Panel image - * - * @internal - * - * @return string - */ - public static function imagePlaceholder(): string - { - return 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw'; - } + /** + * Data URI placeholder string for Panel image + * + * @internal + * + * @return string + */ + public static function imagePlaceholder(): string + { + return 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw'; + } - /** - * Returns the image file object based on provided query - * - * @internal - * - * @param string|null $query - * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null - */ - protected function imageSource(?string $query = null) - { - $image = $this->model->query($query ?? null); + /** + * Returns the image file object based on provided query + * + * @internal + * + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null + */ + protected function imageSource(?string $query = null) + { + $image = $this->model->query($query ?? null); - // validate the query result - if ( - is_a($image, 'Kirby\Cms\File') === true || - is_a($image, 'Kirby\Filesystem\Asset') === true - ) { - return $image; - } + // validate the query result + if ( + is_a($image, 'Kirby\Cms\File') === true || + is_a($image, 'Kirby\Filesystem\Asset') === true + ) { + return $image; + } - return null; - } + return null; + } - /** - * Checks for disabled dropdown options according - * to the given permissions - * - * @param string $action - * @param array $options - * @param array $permissions - * @return bool - */ - public function isDisabledDropdownOption(string $action, array $options, array $permissions): bool - { - $option = $options[$action] ?? true; - return $permissions[$action] === false || $option === false || $option === 'false'; - } + /** + * Checks for disabled dropdown options according + * to the given permissions + * + * @param string $action + * @param array $options + * @param array $permissions + * @return bool + */ + public function isDisabledDropdownOption(string $action, array $options, array $permissions): bool + { + $option = $options[$action] ?? true; + return $permissions[$action] === false || $option === false || $option === 'false'; + } - /** - * Returns lock info for the Panel - * - * @return array|false array with lock info, - * false if locking is not supported - */ - public function lock() - { - if ($lock = $this->model->lock()) { - if ($lock->isUnlocked() === true) { - return ['state' => 'unlock']; - } + /** + * Returns lock info for the Panel + * + * @return array|false array with lock info, + * false if locking is not supported + */ + public function lock() + { + if ($lock = $this->model->lock()) { + if ($lock->isUnlocked() === true) { + return ['state' => 'unlock']; + } - if ($lock->isLocked() === true) { - return [ - 'state' => 'lock', - 'data' => $lock->get() - ]; - } + if ($lock->isLocked() === true) { + return [ + 'state' => 'lock', + 'data' => $lock->get() + ]; + } - return ['state' => null]; - } + return ['state' => null]; + } - return false; - } + return false; + } - /** - * Returns an array of all actions - * that can be performed in the Panel - * This also checks for the lock status - * - * @param array $unlock An array of options that will be force-unlocked - * @return array - */ - public function options(array $unlock = []): array - { - $options = $this->model->permissions()->toArray(); + /** + * Returns an array of all actions + * that can be performed in the Panel + * This also checks for the lock status + * + * @param array $unlock An array of options that will be force-unlocked + * @return array + */ + public function options(array $unlock = []): array + { + $options = $this->model->permissions()->toArray(); - if ($this->model->isLocked()) { - foreach ($options as $key => $value) { - if (in_array($key, $unlock)) { - continue; - } + if ($this->model->isLocked()) { + foreach ($options as $key => $value) { + if (in_array($key, $unlock)) { + continue; + } - $options[$key] = false; - } - } + $options[$key] = false; + } + } - return $options; - } + return $options; + } - /** - * Returns the full path without leading slash - * - * @return string - */ - abstract public function path(): string; + /** + * Returns the full path without leading slash + * + * @return string + */ + abstract public function path(): string; - /** - * Prepares the response data for page pickers - * and page fields - * - * @param array|null $params - * @return array - */ - public function pickerData(array $params = []): array - { - return [ - 'id' => $this->model->id(), - 'image' => $this->image( - $params['image'] ?? [], - $params['layout'] ?? 'list' - ), - 'info' => $this->model->toSafeString($params['info'] ?? false), - 'link' => $this->url(true), - 'sortable' => true, - 'text' => $this->model->toSafeString($params['text'] ?? false), - ]; - } + /** + * Prepares the response data for page pickers + * and page fields + * + * @param array|null $params + * @return array + */ + public function pickerData(array $params = []): array + { + return [ + 'id' => $this->model->id(), + 'image' => $this->image( + $params['image'] ?? [], + $params['layout'] ?? 'list' + ), + 'info' => $this->model->toSafeString($params['info'] ?? false), + 'link' => $this->url(true), + 'sortable' => true, + 'text' => $this->model->toSafeString($params['text'] ?? false), + ]; + } - /** - * Returns the data array for the - * view's component props - * - * @internal - * - * @return array - */ - public function props(): array - { - $blueprint = $this->model->blueprint(); - $tabs = $blueprint->tabs(); - $tab = $blueprint->tab(get('tab')) ?? $tabs[0] ?? null; + /** + * Returns the data array for the + * view's component props + * + * @internal + * + * @return array + */ + public function props(): array + { + $blueprint = $this->model->blueprint(); + $request = $this->model->kirby()->request(); + $tabs = $blueprint->tabs(); + $tab = $blueprint->tab($request->get('tab')) ?? $tabs[0] ?? null; - $props = [ - 'lock' => $this->lock(), - 'permissions' => $this->model->permissions()->toArray(), - 'tabs' => $tabs, - ]; + $props = [ + 'lock' => $this->lock(), + 'permissions' => $this->model->permissions()->toArray(), + 'tabs' => $tabs, + ]; - // only send the tab if it exists - // this will let the vue component define - // a proper default value - if ($tab) { - $props['tab'] = $tab; - } + // only send the tab if it exists + // this will let the vue component define + // a proper default value + if ($tab) { + $props['tab'] = $tab; + } - return $props; - } + return $props; + } - /** - * Returns link url and tooltip - * for model (e.g. used for prev/next - * navigation) - * - * @internal - * - * @param string $tooltip - * @return array - */ - public function toLink(string $tooltip = 'title'): array - { - return [ - 'link' => $this->url(true), - 'tooltip' => (string)$this->model->{$tooltip}() - ]; - } + /** + * Returns link url and tooltip + * for model (e.g. used for prev/next + * navigation) + * + * @internal + * + * @param string $tooltip + * @return array + */ + public function toLink(string $tooltip = 'title'): array + { + return [ + 'link' => $this->url(true), + 'tooltip' => (string)$this->model->{$tooltip}() + ]; + } - /** - * Returns the url to the editing view - * in the Panel - * - * @internal - * - * @param bool $relative - * @return string - */ - public function url(bool $relative = false): string - { - if ($relative === true) { - return '/' . $this->path(); - } + /** + * Returns link url and tooltip + * for optional sibling model and + * preserves tab selection + * + * @internal + * + * @param \Kirby\Cms\ModelWithContent|null $model + * @param string $tooltip + * @return array + */ + protected function toPrevNextLink($model = null, string $tooltip = 'title'): ?array + { + if ($model === null) { + return null; + } - return $this->model->kirby()->url('panel') . '/' . $this->path(); - } + $data = $model->panel()->toLink($tooltip); - /** - * Returns the data array for - * this model's Panel view - * - * @internal - * - * @return array - */ - abstract public function view(): array; + if ($tab = $model->kirby()->request()->get('tab')) { + $uri = new Uri($data['link'], [ + 'query' => ['tab' => $tab] + ]); + + $data['link'] = $uri->toString(); + } + + return $data; + } + + /** + * Returns the url to the editing view + * in the Panel + * + * @internal + * + * @param bool $relative + * @return string + */ + public function url(bool $relative = false): string + { + if ($relative === true) { + return '/' . $this->path(); + } + + return $this->model->kirby()->url('panel') . '/' . $this->path(); + } + + /** + * Returns the data array for + * this model's Panel view + * + * @internal + * + * @return array + */ + abstract public function view(): array; } diff --git a/kirby/src/Panel/Page.php b/kirby/src/Panel/Page.php index e60d6ba..9d73016 100644 --- a/kirby/src/Panel/Page.php +++ b/kirby/src/Panel/Page.php @@ -2,6 +2,8 @@ namespace Kirby\Panel; +use Kirby\Toolkit\I18n; + /** * Provides information about the page model for the Panel * @since 3.6.0 @@ -14,364 +16,360 @@ namespace Kirby\Panel; */ class Page extends Model { - /** - * Breadcrumb array - * - * @return array - */ - public function breadcrumb(): array - { - $parents = $this->model->parents()->flip()->merge($this->model); - return $parents->values(fn ($parent) => [ - 'label' => $parent->title()->toString(), - 'link' => $parent->panel()->url(true), - ]); - } + /** + * @var \Kirby\Cms\Page + */ + protected $model; - /** - * Provides a kirbytag or markdown - * tag for the page, which will be - * used in the panel, when the page - * gets dragged onto a textarea - * - * @internal - * @param string|null $type (`auto`|`kirbytext`|`markdown`) - * @return string - */ - public function dragText(string $type = null): string - { - $type = $this->dragTextType($type); + /** + * Breadcrumb array + * + * @return array + */ + public function breadcrumb(): array + { + $parents = $this->model->parents()->flip()->merge($this->model); + return $parents->values(fn ($parent) => [ + 'label' => $parent->title()->toString(), + 'link' => $parent->panel()->url(true), + ]); + } - if ($callback = $this->dragTextFromCallback($type)) { - return $callback; - } + /** + * Provides a kirbytag or markdown + * tag for the page, which will be + * used in the panel, when the page + * gets dragged onto a textarea + * + * @internal + * @param string|null $type (`auto`|`kirbytext`|`markdown`) + * @return string + */ + public function dragText(string $type = null): string + { + $type = $this->dragTextType($type); - if ($type === 'markdown') { - return '[' . $this->model->title() . '](' . $this->model->url() . ')'; - } + if ($callback = $this->dragTextFromCallback($type)) { + return $callback; + } - return '(link: ' . $this->model->id() . ' text: ' . $this->model->title() . ')'; - } + if ($type === 'markdown') { + return '[' . $this->model->title() . '](' . $this->model->url() . ')'; + } - /** - * Provides options for the page dropdown - * - * @param array $options - * @return array - */ - public function dropdown(array $options = []): array - { - $defaults = [ - 'view' => get('view'), - 'sort' => get('sort'), - 'delete' => get('delete') - ]; + return '(link: ' . $this->model->id() . ' text: ' . $this->model->title() . ')'; + } - $options = array_merge($defaults, $options); - $page = $this->model; - $permissions = $this->options(['preview']); - $view = $options['view'] ?? 'view'; - $url = $this->url(true); - $result = []; + /** + * Provides options for the page dropdown + * + * @param array $options + * @return array + */ + public function dropdown(array $options = []): array + { + $page = $this->model; - if ($view === 'list') { - $result['preview'] = [ - 'link' => $page->previewUrl(), - 'target' => '_blank', - 'icon' => 'open', - 'text' => t('open'), - 'disabled' => $this->isDisabledDropdownOption('preview', $options, $permissions) - ]; - $result[] = '-'; - } + $defaults = $page->kirby()->request()->get(['view', 'sort', 'delete']); + $options = array_merge($defaults, $options); - $result['changeTitle'] = [ - 'dialog' => [ - 'url' => $url . '/changeTitle', - 'query' => [ - 'select' => 'title' - ] - ], - 'icon' => 'title', - 'text' => t('rename'), - 'disabled' => $this->isDisabledDropdownOption('changeTitle', $options, $permissions) - ]; + $permissions = $this->options(['preview']); + $view = $options['view'] ?? 'view'; + $url = $this->url(true); + $result = []; - $result['duplicate'] = [ - 'dialog' => $url . '/duplicate', - 'icon' => 'copy', - 'text' => t('duplicate'), - 'disabled' => $this->isDisabledDropdownOption('duplicate', $options, $permissions) - ]; + if ($view === 'list') { + $result['preview'] = [ + 'link' => $page->previewUrl(), + 'target' => '_blank', + 'icon' => 'open', + 'text' => I18n::translate('open'), + 'disabled' => $this->isDisabledDropdownOption('preview', $options, $permissions) + ]; + $result[] = '-'; + } - $result[] = '-'; + $result['changeTitle'] = [ + 'dialog' => [ + 'url' => $url . '/changeTitle', + 'query' => [ + 'select' => 'title' + ] + ], + 'icon' => 'title', + 'text' => I18n::translate('rename'), + 'disabled' => $this->isDisabledDropdownOption('changeTitle', $options, $permissions) + ]; - $result['changeSlug'] = [ - 'dialog' => [ - 'url' => $url . '/changeTitle', - 'query' => [ - 'select' => 'slug' - ] - ], - 'icon' => 'url', - 'text' => t('page.changeSlug'), - 'disabled' => $this->isDisabledDropdownOption('changeSlug', $options, $permissions) - ]; + $result['duplicate'] = [ + 'dialog' => $url . '/duplicate', + 'icon' => 'copy', + 'text' => I18n::translate('duplicate'), + 'disabled' => $this->isDisabledDropdownOption('duplicate', $options, $permissions) + ]; - $result['changeStatus'] = [ - 'dialog' => $url . '/changeStatus', - 'icon' => 'preview', - 'text' => t('page.changeStatus'), - 'disabled' => $this->isDisabledDropdownOption('changeStatus', $options, $permissions) - ]; + $result[] = '-'; - $siblings = $page->parentModel()->children()->listed()->not($page); + $result['changeSlug'] = [ + 'dialog' => [ + 'url' => $url . '/changeTitle', + 'query' => [ + 'select' => 'slug' + ] + ], + 'icon' => 'url', + 'text' => I18n::translate('page.changeSlug'), + 'disabled' => $this->isDisabledDropdownOption('changeSlug', $options, $permissions) + ]; - $result['changeSort'] = [ - 'dialog' => $url . '/changeSort', - 'icon' => 'sort', - 'text' => t('page.sort'), - 'disabled' => $siblings->count() === 0 || $this->isDisabledDropdownOption('sort', $options, $permissions) - ]; + $result['changeStatus'] = [ + 'dialog' => $url . '/changeStatus', + 'icon' => 'preview', + 'text' => I18n::translate('page.changeStatus'), + 'disabled' => $this->isDisabledDropdownOption('changeStatus', $options, $permissions) + ]; - $result['changeTemplate'] = [ - 'dialog' => $url . '/changeTemplate', - 'icon' => 'template', - 'text' => t('page.changeTemplate'), - 'disabled' => $this->isDisabledDropdownOption('changeTemplate', $options, $permissions) - ]; + $siblings = $page->parentModel()->children()->listed()->not($page); - $result[] = '-'; - $result['delete'] = [ - 'dialog' => $url . '/delete', - 'icon' => 'trash', - 'text' => t('delete'), - 'disabled' => $this->isDisabledDropdownOption('delete', $options, $permissions) - ]; + $result['changeSort'] = [ + 'dialog' => $url . '/changeSort', + 'icon' => 'sort', + 'text' => I18n::translate('page.sort'), + 'disabled' => $siblings->count() === 0 || $this->isDisabledDropdownOption('sort', $options, $permissions) + ]; - return $result; - } + $result['changeTemplate'] = [ + 'dialog' => $url . '/changeTemplate', + 'icon' => 'template', + 'text' => I18n::translate('page.changeTemplate'), + 'disabled' => $this->isDisabledDropdownOption('changeTemplate', $options, $permissions) + ]; - /** - * Returns the setup for a dropdown option - * which is used in the changes dropdown - * for example. - * - * @return array - */ - public function dropdownOption(): array - { - return [ - 'text' => $this->model->title()->value(), - ] + parent::dropdownOption(); - } + $result[] = '-'; + $result['delete'] = [ + 'dialog' => $url . '/delete', + 'icon' => 'trash', + 'text' => I18n::translate('delete'), + 'disabled' => $this->isDisabledDropdownOption('delete', $options, $permissions) + ]; - /** - * Returns the escaped Id, which is - * used in the panel to make routing work properly - * - * @return string - */ - public function id(): string - { - return str_replace('/', '+', $this->model->id()); - } + return $result; + } - /** - * Default settings for the page's Panel image - * - * @return array - */ - protected function imageDefaults(): array - { - $defaults = []; + /** + * Returns the setup for a dropdown option + * which is used in the changes dropdown + * for example. + * + * @return array + */ + public function dropdownOption(): array + { + return [ + 'text' => $this->model->title()->value(), + ] + parent::dropdownOption(); + } - if ($icon = $this->model->blueprint()->icon()) { - $defaults['icon'] = $icon; - } + /** + * Returns the escaped Id, which is + * used in the panel to make routing work properly + * + * @return string + */ + public function id(): string + { + return str_replace('/', '+', $this->model->id()); + } - return array_merge(parent::imageDefaults(), $defaults); - } + /** + * Default settings for the page's Panel image + * + * @return array + */ + protected function imageDefaults(): array + { + $defaults = []; - /** - * Returns the image file object based on provided query - * - * @internal - * @param string|null $query - * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null - */ - protected function imageSource(string $query = null) - { - if ($query === null) { - $query = 'page.image'; - } + if ($icon = $this->model->blueprint()->icon()) { + $defaults['icon'] = $icon; + } - return parent::imageSource($query); - } + return array_merge(parent::imageDefaults(), $defaults); + } - /** - * Returns the full path without leading slash - * - * @internal - * @return string - */ - public function path(): string - { - return 'pages/' . $this->id(); - } + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null + */ + protected function imageSource(string $query = null) + { + if ($query === null) { + $query = 'page.image'; + } - /** - * Prepares the response data for page pickers - * and page fields - * - * @param array|null $params - * @return array - */ - public function pickerData(array $params = []): array - { - $params['text'] ??= '{{ page.title }}'; + return parent::imageSource($query); + } - return array_merge(parent::pickerData($params), [ - 'dragText' => $this->dragText(), - 'hasChildren' => $this->model->hasChildren(), - 'url' => $this->model->url() - ]); - } + /** + * Returns the full path without leading slash + * + * @internal + * @return string + */ + public function path(): string + { + return 'pages/' . $this->id(); + } - /** - * The best applicable position for - * the position/status dialog - * - * @return int - */ - public function position(): int - { - return $this->model->num() ?? $this->model->parentModel()->children()->listed()->not($this->model)->count() + 1; - } + /** + * Prepares the response data for page pickers + * and page fields + * + * @param array|null $params + * @return array + */ + public function pickerData(array $params = []): array + { + $params['text'] ??= '{{ page.title }}'; - /** - * Returns navigation array with - * previous and next page - * based on blueprint definition - * - * @internal - * - * @return array - */ - public function prevNext(): array - { - $page = $this->model; + return array_merge(parent::pickerData($params), [ + 'dragText' => $this->dragText(), + 'hasChildren' => $this->model->hasChildren(), + 'url' => $this->model->url() + ]); + } - // create siblings collection based on - // blueprint navigation - $siblings = function (string $direction) use ($page) { - $navigation = $page->blueprint()->navigation(); - $sortBy = $navigation['sortBy'] ?? null; - $status = $navigation['status'] ?? null; - $template = $navigation['template'] ?? null; - $direction = $direction === 'prev' ? 'prev' : 'next'; + /** + * The best applicable position for + * the position/status dialog + * + * @return int + */ + public function position(): int + { + return $this->model->num() ?? $this->model->parentModel()->children()->listed()->not($this->model)->count() + 1; + } - // if status is defined in navigation, - // all items in the collection are used - // (drafts, listed and unlisted) otherwise - // it depends on the status of the page - $siblings = $status !== null ? $page->parentModel()->childrenAndDrafts() : $page->siblings(); + /** + * Returns navigation array with + * previous and next page + * based on blueprint definition + * + * @internal + * + * @return array + */ + public function prevNext(): array + { + $page = $this->model; - // sort the collection if custom sortBy - // defined in navigation otherwise - // default sorting will apply - if ($sortBy !== null) { - $siblings = $siblings->sort(...$siblings::sortArgs($sortBy)); - } + // create siblings collection based on + // blueprint navigation + $siblings = function (string $direction) use ($page) { + $navigation = $page->blueprint()->navigation(); + $sortBy = $navigation['sortBy'] ?? null; + $status = $navigation['status'] ?? null; + $template = $navigation['template'] ?? null; + $direction = $direction === 'prev' ? 'prev' : 'next'; - $siblings = $page->{$direction . 'All'}($siblings); + // if status is defined in navigation, + // all items in the collection are used + // (drafts, listed and unlisted) otherwise + // it depends on the status of the page + $siblings = $status !== null ? $page->parentModel()->childrenAndDrafts() : $page->siblings(); - if (empty($navigation) === false) { - $statuses = (array)($status ?? $page->status()); - $templates = (array)($template ?? $page->intendedTemplate()); + // sort the collection if custom sortBy + // defined in navigation otherwise + // default sorting will apply + if ($sortBy !== null) { + $siblings = $siblings->sort(...$siblings::sortArgs($sortBy)); + } - // do not filter if template navigation is all - if (in_array('all', $templates) === false) { - $siblings = $siblings->filter('intendedTemplate', 'in', $templates); - } + $siblings = $page->{$direction . 'All'}($siblings); - // do not filter if status navigation is all - if (in_array('all', $statuses) === false) { - $siblings = $siblings->filter('status', 'in', $statuses); - } - } else { - $siblings = $siblings - ->filter('intendedTemplate', $page->intendedTemplate()) - ->filter('status', $page->status()); - } + if (empty($navigation) === false) { + $statuses = (array)($status ?? $page->status()); + $templates = (array)($template ?? $page->intendedTemplate()); - return $siblings->filter('isReadable', true); - }; + // do not filter if template navigation is all + if (in_array('all', $templates) === false) { + $siblings = $siblings->filter('intendedTemplate', 'in', $templates); + } - return [ - 'next' => function () use ($siblings) { - $next = $siblings('next')->first(); - return $next ? $next->panel()->toLink('title') : null; - }, - 'prev' => function () use ($siblings) { - $prev = $siblings('prev')->last(); - return $prev ? $prev->panel()->toLink('title') : null; - } - ]; - } + // do not filter if status navigation is all + if (in_array('all', $statuses) === false) { + $siblings = $siblings->filter('status', 'in', $statuses); + } + } else { + $siblings = $siblings + ->filter('intendedTemplate', $page->intendedTemplate()) + ->filter('status', $page->status()); + } - /** - * Returns the data array for the - * view's component props - * - * @internal - * - * @return array - */ - public function props(): array - { - $page = $this->model; + return $siblings->filter('isReadable', true); + }; - return array_merge( - parent::props(), - $this->prevNext(), - [ - 'blueprint' => $this->model->intendedTemplate()->name(), - 'model' => [ - 'content' => $this->content(), - 'id' => $page->id(), - 'link' => $this->url(true), - 'parent' => $page->parentModel()->panel()->url(true), - 'previewUrl' => $page->previewUrl(), - 'status' => $page->status(), - 'title' => $page->title()->toString(), - ], - 'status' => function () use ($page) { - if ($status = $page->status()) { - return $page->blueprint()->status()[$status] ?? null; - } - }, - ] - ); - } + return [ + 'next' => fn () => $this->toPrevNextLink($siblings('next')->first()), + 'prev' => fn () => $this->toPrevNextLink($siblings('prev')->last()) + ]; + } - /** - * Returns the data array for - * this model's Panel view - * - * @internal - * - * @return array - */ - public function view(): array - { - $page = $this->model; + /** + * Returns the data array for the + * view's component props + * + * @internal + * + * @return array + */ + public function props(): array + { + $page = $this->model; - return [ - 'breadcrumb' => $page->panel()->breadcrumb(), - 'component' => 'k-page-view', - 'props' => $this->props(), - 'title' => $page->title()->toString(), - ]; - } + return array_merge( + parent::props(), + $this->prevNext(), + [ + 'blueprint' => $this->model->intendedTemplate()->name(), + 'model' => [ + 'content' => $this->content(), + 'id' => $page->id(), + 'link' => $this->url(true), + 'parent' => $page->parentModel()->panel()->url(true), + 'previewUrl' => $page->previewUrl(), + 'status' => $page->status(), + 'title' => $page->title()->toString(), + ], + 'status' => function () use ($page) { + if ($status = $page->status()) { + return $page->blueprint()->status()[$status] ?? null; + } + }, + ] + ); + } + + /** + * Returns the data array for + * this model's Panel view + * + * @internal + * + * @return array + */ + public function view(): array + { + $page = $this->model; + + return [ + 'breadcrumb' => $page->panel()->breadcrumb(), + 'component' => 'k-page-view', + 'props' => $this->props(), + 'title' => $page->title()->toString(), + ]; + } } diff --git a/kirby/src/Panel/Panel.php b/kirby/src/Panel/Panel.php index 2a8dfe0..f237478 100644 --- a/kirby/src/Panel/Panel.php +++ b/kirby/src/Panel/Panel.php @@ -2,11 +2,14 @@ namespace Kirby\Panel; +use Kirby\Cms\App; +use Kirby\Cms\Url as CmsUrl; use Kirby\Cms\User; use Kirby\Exception\Exception; use Kirby\Exception\NotFoundException; use Kirby\Exception\PermissionException; use Kirby\Http\Response; +use Kirby\Http\Router; use Kirby\Http\Url; use Kirby\Toolkit\Str; use Kirby\Toolkit\Tpl; @@ -27,585 +30,591 @@ use Throwable; */ class Panel { - /** - * Normalize a panel area - * - * @param string $id - * @param array|string $area - * @return array - */ - public static function area(string $id, $area): array - { - $area['id'] = $id; - $area['label'] ??= $id; - $area['breadcrumb'] ??= []; - $area['breadcrumbLabel'] ??= $area['label']; - $area['title'] = $area['label']; - $area['menu'] ??= false; - $area['link'] ??= $id; - $area['search'] ??= null; - - return $area; - } - - /** - * Collect all registered areas - * - * @return array - */ - public static function areas(): array - { - $kirby = kirby(); - $system = $kirby->system(); - $user = $kirby->user(); - $areas = $kirby->load()->areas(); - - // the system is not ready - if ($system->isOk() === false || $system->isInstalled() === false) { - return [ - 'installation' => static::area('installation', $areas['installation']), - ]; - } - - // not yet authenticated - if (!$user) { - return [ - 'login' => static::area('login', $areas['login']), - ]; - } - - unset($areas['installation'], $areas['login']); - - // Disable the language area for single-language installations - // This does not check for installed languages. Otherwise you'd - // not be able to add the first language through the view - if (!$kirby->option('languages')) { - unset($areas['languages']); - } - - $menu = $kirby->option('panel.menu', [ - 'site', - 'languages', - 'users', - 'system', - ]); - - $result = []; - - // add the sorted areas - foreach ($menu as $id) { - if ($area = ($areas[$id] ?? null)) { - $result[$id] = static::area($id, $area); - unset($areas[$id]); - } - } - - // add the remaining areas - foreach ($areas as $id => $area) { - $result[$id] = static::area($id, $area); - } - - return $result; - } - - /** - * Check for access permissions - * - * @param \Kirby\Cms\User|null $user - * @param string|null $areaId - * @return bool - */ - public static function firewall(?User $user = null, ?string $areaId = null): bool - { - // a user has to be logged in - if ($user === null) { - throw new PermissionException(['key' => 'access.panel']); - } - - // get all access permissions for the user role - $permissions = $user->role()->permissions()->toArray()['access']; - - // check for general panel access - if (($permissions['panel'] ?? true) !== true) { - throw new PermissionException(['key' => 'access.panel']); - } - - // don't check if the area is not defined - if (empty($areaId) === true) { - return true; - } - - // undefined area permissions means access - if (isset($permissions[$areaId]) === false) { - return true; - } - - // no access - if ($permissions[$areaId] !== true) { - throw new PermissionException(['key' => 'access.view']); - } - - return true; - } - - - /** - * Redirect to a Panel url - * - * @param string|null $path - * @param int $code - * @throws \Kirby\Panel\Redirect - * @return void - * @codeCoverageIgnore - */ - public static function go(?string $url = null, int $code = 302): void - { - throw new Redirect(static::url($url), $code); - } - - /** - * Check if the given user has access to the panel - * or to a given area - * - * @param \Kirby\Cms\User|null $user - * @param string|null $area - * @return bool - */ - public static function hasAccess(?User $user = null, string $area = null): bool - { - try { - static::firewall($user, $area); - return true; - } catch (Throwable $e) { - return false; - } - } - - /** - * Checks for a Fiber request - * via get parameters or headers - * - * @return bool - */ - public static function isFiberRequest(): bool - { - $request = kirby()->request(); - - if ($request->method() === 'GET') { - return (bool)($request->get('_json') ?? $request->header('X-Fiber')); - } - - return false; - } - - /** - * Returns a JSON response - * for Fiber calls - * - * @param array $data - * @param int $code - * @return \Kirby\Http\Response - */ - public static function json(array $data, int $code = 200) - { - return Response::json($data, $code, get('_pretty'), [ - 'X-Fiber' => 'true', - 'Cache-Control' => 'no-store' - ]); - } - - /** - * Checks for a multilanguage installation - * - * @return bool - */ - public static function multilang(): bool - { - // multilang setup check - $kirby = kirby(); - return $kirby->option('languages') || $kirby->multilang(); - } - - /** - * Returns the referrer path if present - * - * @return string - */ - public static function referrer(): string - { - $referrer = kirby()->request()->header('X-Fiber-Referrer') - ?? get('_referrer') - ?? ''; - - return '/' . trim($referrer, '/'); - } - - /** - * Creates a Response object from the result of - * a Panel route call - * - * @params mixed $result - * @params array $options - * @return \Kirby\Http\Response - */ - public static function response($result, array $options = []) - { - // pass responses directly down to the Kirby router - if (is_a($result, 'Kirby\Http\Response') === true) { - return $result; - } - - // interpret missing/empty results as not found - if ($result === null || $result === false) { - $result = new NotFoundException('The data could not be found'); - - // interpret strings as errors - } elseif (is_string($result) === true) { - $result = new Exception($result); - } - - // handle different response types (view, dialog, ...) - switch ($options['type'] ?? null) { - case 'dialog': - return Dialog::response($result, $options); - case 'dropdown': - return Dropdown::response($result, $options); - case 'search': - return Search::response($result, $options); - default: - return View::response($result, $options); - } - } - - /** - * Router for the Panel views - * - * @param string $path - * @return \Kirby\Http\Response|false - */ - public static function router(string $path = null) - { - $kirby = kirby(); - - if ($kirby->option('panel') === false) { - return null; - } - - // set the translation for Panel UI before - // gathering areas and routes, so that the - // `t()` helper can already be used - static::setTranslation(); - - // set the language in multi-lang installations - static::setLanguage(); - - $areas = static::areas(); - $routes = static::routes($areas); - - // create a micro-router for the Panel - return router($path, $method = $kirby->request()->method(), $routes, function ($route) use ($areas, $kirby, $method, $path) { - - // route needs authentication? - $auth = $route->attributes()['auth'] ?? true; - $areaId = $route->attributes()['area'] ?? null; - $type = $route->attributes()['type'] ?? 'view'; - $area = $areas[$areaId] ?? null; - - // call the route action to check the result - try { - // trigger hook - $route = $kirby->apply('panel.route:before', compact('route', 'path', 'method'), 'route'); - - // check for access before executing area routes - if ($auth !== false) { - static::firewall($kirby->user(), $areaId); - } - - $result = $route->action()->call($route, ...$route->arguments()); - } catch (Throwable $e) { - $result = $e; - } - - $response = static::response($result, [ - 'area' => $area, - 'areas' => $areas, - 'path' => $path, - 'type' => $type - ]); - - return $kirby->apply('panel.route:after', compact('route', 'path', 'method', 'response'), 'response'); - }); - } - - /** - * Extract the routes from the given array - * of active areas. - * - * @return array - */ - public static function routes(array $areas): array - { - $kirby = kirby(); - - // the browser incompatibility - // warning is always needed - $routes = [ - [ - 'pattern' => 'browser', - 'auth' => false, - 'action' => fn () => new Response( - Tpl::load($kirby->root('kirby') . '/views/browser.php') - ), - ] - ]; - - // register all routes from areas - foreach ($areas as $areaId => $area) { - $routes = array_merge( - $routes, - static::routesForViews($areaId, $area), - static::routesForSearches($areaId, $area), - static::routesForDialogs($areaId, $area), - static::routesForDropdowns($areaId, $area), - ); - } - - // if the Panel is already installed and/or the - // user is authenticated, those areas won't be - // included, which is why we add redirect routes - // to main Panel view as fallbacks - $routes[] = [ - 'pattern' => [ - '/', - 'installation', - 'login', - ], - 'action' => fn () => Panel::go(Home::url()), - 'auth' => false - ]; - - // catch all route - $routes[] = [ - 'pattern' => '(:all)', - 'action' => fn () => 'The view could not be found' - ]; - - return $routes; - } - - /** - * Extract all routes from an area - * - * @param string $areaId - * @param array $area - * @return array - */ - public static function routesForDialogs(string $areaId, array $area): array - { - $dialogs = $area['dialogs'] ?? []; - $routes = []; - - foreach ($dialogs as $key => $dialog) { - - // create the full pattern with dialogs prefix - $pattern = 'dialogs/' . trim(($dialog['pattern'] ?? $key), '/'); - - // load event - $routes[] = [ - 'pattern' => $pattern, - 'type' => 'dialog', - 'area' => $areaId, - 'action' => $dialog['load'] ?? fn () => 'The load handler for your dialog is missing' - ]; - - // submit event - $routes[] = [ - 'pattern' => $pattern, - 'type' => 'dialog', - 'area' => $areaId, - 'method' => 'POST', - 'action' => $dialog['submit'] ?? fn () => 'Your dialog does not define a submit handler' - ]; - } - - return $routes; - } - - /** - * Extract all routes for dropdowns - * - * @param string $areaId - * @param array $area - * @return array - */ - public static function routesForDropdowns(string $areaId, array $area): array - { - $dropdowns = $area['dropdowns'] ?? []; - $routes = []; - - foreach ($dropdowns as $name => $dropdown) { - // Handle shortcuts for dropdowns. The name is the pattern - // and options are defined in a Closure - if (is_a($dropdown, 'Closure') === true) { - $dropdown = [ - 'pattern' => $name, - 'action' => $dropdown - ]; - } - - // create the full pattern with dropdowns prefix - $pattern = 'dropdowns/' . trim(($dropdown['pattern'] ?? $name), '/'); - - // load event - $routes[] = [ - 'pattern' => $pattern, - 'type' => 'dropdown', - 'area' => $areaId, - 'method' => 'GET|POST', - 'action' => $dropdown['options'] ?? $dropdown['action'] - ]; - } - - return $routes; - } - - /** - * Extract all routes for searches - * - * @param string $areaId - * @param array $area - * @return array - */ - public static function routesForSearches(string $areaId, array $area): array - { - $searches = $area['searches'] ?? []; - $routes = []; - - foreach ($searches as $name => $params) { - - // create the full routing pattern - $pattern = 'search/' . $name; - - // load event - $routes[] = [ - 'pattern' => $pattern, - 'type' => 'search', - 'area' => $areaId, - 'action' => function () use ($params) { - return $params['query'](get('query')); - } - ]; - } - - return $routes; - } - - /** - * Extract all views from an area - * - * @param string $areaId - * @param array $area - * @return array - */ - public static function routesForViews(string $areaId, array $area): array - { - $views = $area['views'] ?? []; - $routes = []; - - foreach ($views as $view) { - $view['area'] = $areaId; - $view['type'] = 'view'; - $routes[] = $view; - } - - return $routes; - } - - /** - * Set the current language in multi-lang - * installations based on the session or the - * query language query parameter - * - * @return string|null - */ - public static function setLanguage(): ?string - { - $kirby = kirby(); - - // language switcher - if (static::multilang()) { - $fallback = 'en'; - - if ($defaultLanguage = $kirby->defaultLanguage()) { - $fallback = $defaultLanguage->code(); - } - - $session = $kirby->session(); - $sessionLanguage = $session->get('panel.language', $fallback); - $language = get('language') ?? $sessionLanguage; - - // keep the language for the next visit - if ($language !== $sessionLanguage) { - $session->set('panel.language', $language); - } - - // activate the current language in Kirby - $kirby->setCurrentLanguage($language); - - return $language; - } - - return null; - } - - /** - * Set the currently active Panel translation - * based on the current user or config - * - * @return string - */ - public static function setTranslation(): string - { - $kirby = kirby(); - - if ($user = $kirby->user()) { - // use the user language for the default translation - $translation = $user->language(); - } else { - // fall back to the language from the config - $translation = $kirby->panelLanguage(); - } - - $kirby->setCurrentTranslation($translation); - - return $translation; - } - - /** - * Creates an absolute Panel URL - * independent of the Panel slug config - * - * @param string|null $url - * @return string - */ - public static function url(?string $url = null): string - { - $slug = kirby()->option('panel.slug', 'panel'); - - // only touch relative paths - if (Url::isAbsolute($url) === false) { - $path = trim($url, '/'); - - // add the panel slug prefix if it it's not - // included in the path yet - if (Str::startsWith($path, $slug . '/') === false) { - $path = $slug . '/' . $path; - } - - // create an absolute URL - $url = url($path); - } - - return $url; - } + /** + * Normalize a panel area + * + * @param string $id + * @param array|string $area + * @return array + */ + public static function area(string $id, $area): array + { + $area['id'] = $id; + $area['label'] ??= $id; + $area['breadcrumb'] ??= []; + $area['breadcrumbLabel'] ??= $area['label']; + $area['title'] = $area['label']; + $area['menu'] ??= false; + $area['link'] ??= $id; + $area['search'] ??= null; + + return $area; + } + + /** + * Collect all registered areas + * + * @return array + */ + public static function areas(): array + { + $kirby = App::instance(); + $system = $kirby->system(); + $user = $kirby->user(); + $areas = $kirby->load()->areas(); + + // the system is not ready + if ($system->isOk() === false || $system->isInstalled() === false) { + return [ + 'installation' => static::area('installation', $areas['installation']), + ]; + } + + // not yet authenticated + if (!$user) { + return [ + 'logout' => static::area('logout', $areas['logout']), + + // login area last because it defines a fallback route + 'login' => static::area('login', $areas['login']), + ]; + } + + unset($areas['installation'], $areas['login']); + + // Disable the language area for single-language installations + // This does not check for installed languages. Otherwise you'd + // not be able to add the first language through the view + if (!$kirby->option('languages')) { + unset($areas['languages']); + } + + $menu = $kirby->option('panel.menu', [ + 'site', + 'languages', + 'users', + 'system', + ]); + + $result = []; + + // add the sorted areas + foreach ($menu as $id) { + if ($area = ($areas[$id] ?? null)) { + $result[$id] = static::area($id, $area); + unset($areas[$id]); + } + } + + // add the remaining areas + foreach ($areas as $id => $area) { + $result[$id] = static::area($id, $area); + } + + return $result; + } + + /** + * Check for access permissions + * + * @param \Kirby\Cms\User|null $user + * @param string|null $areaId + * @return bool + */ + public static function firewall(?User $user = null, ?string $areaId = null): bool + { + // a user has to be logged in + if ($user === null) { + throw new PermissionException(['key' => 'access.panel']); + } + + // get all access permissions for the user role + $permissions = $user->role()->permissions()->toArray()['access']; + + // check for general panel access + if (($permissions['panel'] ?? true) !== true) { + throw new PermissionException(['key' => 'access.panel']); + } + + // don't check if the area is not defined + if (empty($areaId) === true) { + return true; + } + + // undefined area permissions means access + if (isset($permissions[$areaId]) === false) { + return true; + } + + // no access + if ($permissions[$areaId] !== true) { + throw new PermissionException(['key' => 'access.view']); + } + + return true; + } + + + /** + * Redirect to a Panel url + * + * @param string|null $path + * @param int $code + * @throws \Kirby\Panel\Redirect + * @return void + * @codeCoverageIgnore + */ + public static function go(?string $url = null, int $code = 302): void + { + throw new Redirect(static::url($url), $code); + } + + /** + * Check if the given user has access to the panel + * or to a given area + * + * @param \Kirby\Cms\User|null $user + * @param string|null $area + * @return bool + */ + public static function hasAccess(?User $user = null, string $area = null): bool + { + try { + static::firewall($user, $area); + return true; + } catch (Throwable $e) { + return false; + } + } + + /** + * Checks for a Fiber request + * via get parameters or headers + * + * @return bool + */ + public static function isFiberRequest(): bool + { + $request = App::instance()->request(); + + if ($request->method() === 'GET') { + return (bool)($request->get('_json') ?? $request->header('X-Fiber')); + } + + return false; + } + + /** + * Returns a JSON response + * for Fiber calls + * + * @param array $data + * @param int $code + * @return \Kirby\Http\Response + */ + public static function json(array $data, int $code = 200) + { + $request = App::instance()->request(); + + return Response::json($data, $code, $request->get('_pretty'), [ + 'X-Fiber' => 'true', + 'Cache-Control' => 'no-store, private' + ]); + } + + /** + * Checks for a multilanguage installation + * + * @return bool + */ + public static function multilang(): bool + { + // multilang setup check + $kirby = App::instance(); + return $kirby->option('languages') || $kirby->multilang(); + } + + /** + * Returns the referrer path if present + * + * @return string + */ + public static function referrer(): string + { + $request = App::instance()->request(); + + $referrer = $request->header('X-Fiber-Referrer') + ?? $request->get('_referrer') + ?? ''; + + return '/' . trim($referrer, '/'); + } + + /** + * Creates a Response object from the result of + * a Panel route call + * + * @params mixed $result + * @params array $options + * @return \Kirby\Http\Response + */ + public static function response($result, array $options = []) + { + // pass responses directly down to the Kirby router + if (is_a($result, 'Kirby\Http\Response') === true) { + return $result; + } + + // interpret missing/empty results as not found + if ($result === null || $result === false) { + $result = new NotFoundException('The data could not be found'); + + // interpret strings as errors + } elseif (is_string($result) === true) { + $result = new Exception($result); + } + + // handle different response types (view, dialog, ...) + switch ($options['type'] ?? null) { + case 'dialog': + return Dialog::response($result, $options); + case 'dropdown': + return Dropdown::response($result, $options); + case 'search': + return Search::response($result, $options); + default: + return View::response($result, $options); + } + } + + /** + * Router for the Panel views + * + * @param string $path + * @return \Kirby\Http\Response|false + */ + public static function router(string $path = null) + { + $kirby = App::instance(); + + if ($kirby->option('panel') === false) { + return null; + } + + // set the translation for Panel UI before + // gathering areas and routes, so that the + // `t()` helper can already be used + static::setTranslation(); + + // set the language in multi-lang installations + static::setLanguage(); + + $areas = static::areas(); + $routes = static::routes($areas); + + // create a micro-router for the Panel + return Router::execute($path, $method = $kirby->request()->method(), $routes, function ($route) use ($areas, $kirby, $method, $path) { + // route needs authentication? + $auth = $route->attributes()['auth'] ?? true; + $areaId = $route->attributes()['area'] ?? null; + $type = $route->attributes()['type'] ?? 'view'; + $area = $areas[$areaId] ?? null; + + // call the route action to check the result + try { + // trigger hook + $route = $kirby->apply('panel.route:before', compact('route', 'path', 'method'), 'route'); + + // check for access before executing area routes + if ($auth !== false) { + static::firewall($kirby->user(), $areaId); + } + + $result = $route->action()->call($route, ...$route->arguments()); + } catch (Throwable $e) { + $result = $e; + } + + $response = static::response($result, [ + 'area' => $area, + 'areas' => $areas, + 'path' => $path, + 'type' => $type + ]); + + return $kirby->apply('panel.route:after', compact('route', 'path', 'method', 'response'), 'response'); + }); + } + + /** + * Extract the routes from the given array + * of active areas. + * + * @return array + */ + public static function routes(array $areas): array + { + $kirby = App::instance(); + + // the browser incompatibility + // warning is always needed + $routes = [ + [ + 'pattern' => 'browser', + 'auth' => false, + 'action' => fn () => new Response( + Tpl::load($kirby->root('kirby') . '/views/browser.php') + ), + ] + ]; + + // register all routes from areas + foreach ($areas as $areaId => $area) { + $routes = array_merge( + $routes, + static::routesForViews($areaId, $area), + static::routesForSearches($areaId, $area), + static::routesForDialogs($areaId, $area), + static::routesForDropdowns($areaId, $area), + ); + } + + // if the Panel is already installed and/or the + // user is authenticated, those areas won't be + // included, which is why we add redirect routes + // to main Panel view as fallbacks + $routes[] = [ + 'pattern' => [ + '/', + 'installation', + 'login', + ], + 'action' => fn () => Panel::go(Home::url()), + 'auth' => false + ]; + + // catch all route + $routes[] = [ + 'pattern' => '(:all)', + 'action' => fn () => 'The view could not be found' + ]; + + return $routes; + } + + /** + * Extract all routes from an area + * + * @param string $areaId + * @param array $area + * @return array + */ + public static function routesForDialogs(string $areaId, array $area): array + { + $dialogs = $area['dialogs'] ?? []; + $routes = []; + + foreach ($dialogs as $key => $dialog) { + // create the full pattern with dialogs prefix + $pattern = 'dialogs/' . trim(($dialog['pattern'] ?? $key), '/'); + + // load event + $routes[] = [ + 'pattern' => $pattern, + 'type' => 'dialog', + 'area' => $areaId, + 'action' => $dialog['load'] ?? fn () => 'The load handler for your dialog is missing' + ]; + + // submit event + $routes[] = [ + 'pattern' => $pattern, + 'type' => 'dialog', + 'area' => $areaId, + 'method' => 'POST', + 'action' => $dialog['submit'] ?? fn () => 'Your dialog does not define a submit handler' + ]; + } + + return $routes; + } + + /** + * Extract all routes for dropdowns + * + * @param string $areaId + * @param array $area + * @return array + */ + public static function routesForDropdowns(string $areaId, array $area): array + { + $dropdowns = $area['dropdowns'] ?? []; + $routes = []; + + foreach ($dropdowns as $name => $dropdown) { + // Handle shortcuts for dropdowns. The name is the pattern + // and options are defined in a Closure + if (is_a($dropdown, 'Closure') === true) { + $dropdown = [ + 'pattern' => $name, + 'action' => $dropdown + ]; + } + + // create the full pattern with dropdowns prefix + $pattern = 'dropdowns/' . trim(($dropdown['pattern'] ?? $name), '/'); + + // load event + $routes[] = [ + 'pattern' => $pattern, + 'type' => 'dropdown', + 'area' => $areaId, + 'method' => 'GET|POST', + 'action' => $dropdown['options'] ?? $dropdown['action'] + ]; + } + + return $routes; + } + + /** + * Extract all routes for searches + * + * @param string $areaId + * @param array $area + * @return array + */ + public static function routesForSearches(string $areaId, array $area): array + { + $searches = $area['searches'] ?? []; + $routes = []; + + foreach ($searches as $name => $params) { + // create the full routing pattern + $pattern = 'search/' . $name; + + // load event + $routes[] = [ + 'pattern' => $pattern, + 'type' => 'search', + 'area' => $areaId, + 'action' => function () use ($params) { + $request = App::instance()->request(); + + return $params['query']($request->get('query')); + } + ]; + } + + return $routes; + } + + /** + * Extract all views from an area + * + * @param string $areaId + * @param array $area + * @return array + */ + public static function routesForViews(string $areaId, array $area): array + { + $views = $area['views'] ?? []; + $routes = []; + + foreach ($views as $view) { + $view['area'] = $areaId; + $view['type'] = 'view'; + $routes[] = $view; + } + + return $routes; + } + + /** + * Set the current language in multi-lang + * installations based on the session or the + * query language query parameter + * + * @return string|null + */ + public static function setLanguage(): ?string + { + $kirby = App::instance(); + + // language switcher + if (static::multilang()) { + $fallback = 'en'; + + if ($defaultLanguage = $kirby->defaultLanguage()) { + $fallback = $defaultLanguage->code(); + } + + $session = $kirby->session(); + $sessionLanguage = $session->get('panel.language', $fallback); + $language = $kirby->request()->get('language') ?? $sessionLanguage; + + // keep the language for the next visit + if ($language !== $sessionLanguage) { + $session->set('panel.language', $language); + } + + // activate the current language in Kirby + $kirby->setCurrentLanguage($language); + + return $language; + } + + return null; + } + + /** + * Set the currently active Panel translation + * based on the current user or config + * + * @return string + */ + public static function setTranslation(): string + { + $kirby = App::instance(); + + if ($user = $kirby->user()) { + // use the user language for the default translation + $translation = $user->language(); + } else { + // fall back to the language from the config + $translation = $kirby->panelLanguage(); + } + + $kirby->setCurrentTranslation($translation); + + return $translation; + } + + /** + * Creates an absolute Panel URL + * independent of the Panel slug config + * + * @param string|null $url + * @return string + */ + public static function url(?string $url = null): string + { + $slug = App::instance()->option('panel.slug', 'panel'); + + // only touch relative paths + if (Url::isAbsolute($url) === false) { + $path = trim($url, '/'); + + // add the panel slug prefix if it it's not + // included in the path yet + if (Str::startsWith($path, $slug . '/') === false) { + $path = $slug . '/' . $path; + } + + // create an absolute URL + $url = CmsUrl::to($path); + } + + return $url; + } } diff --git a/kirby/src/Panel/Plugins.php b/kirby/src/Panel/Plugins.php index 1869099..daa6ff5 100644 --- a/kirby/src/Panel/Plugins.php +++ b/kirby/src/Panel/Plugins.php @@ -3,7 +3,9 @@ namespace Kirby\Panel; use Kirby\Cms\App; +use Kirby\Data\Json; use Kirby\Filesystem\F; +use Kirby\Toolkit\A; use Kirby\Toolkit\Str; /** @@ -19,91 +21,131 @@ use Kirby\Toolkit\Str; */ class Plugins { - /** - * Cache of all collected plugin files - * - * @var array - */ - public $files; + /** + * Cache of all collected plugin files + * + * @var array + */ + public $files; - /** - * Collects and returns the plugin files for all plugins - * - * @return array - */ - public function files(): array - { - if ($this->files !== null) { - return $this->files; - } + /** + * Collects and returns the plugin files for all plugins + * + * @return array + */ + public function files(): array + { + if ($this->files !== null) { + return $this->files; + } - $this->files = []; + $this->files = []; - foreach (App::instance()->plugins() as $plugin) { - $this->files[] = $plugin->root() . '/index.css'; - $this->files[] = $plugin->root() . '/index.js'; - } + foreach (App::instance()->plugins() as $plugin) { + $this->files[] = $plugin->root() . '/index.css'; + $this->files[] = $plugin->root() . '/index.js'; + // During plugin development, kirbyup adds an index.dev.mjs as entry point, which + // Kirby will load instead of the regular index.js. Since kirbyup is based on Vite, + // it can't use the standard index.js as entry for its development server: + // Vite requires an entry of type module so it can use JavaScript imports, + // but Kirbyup needs index.js to load as a regular script, synchronously. + $this->files[] = $plugin->root() . '/index.dev.mjs'; + } - return $this->files; - } + return $this->files; + } - /** - * Returns the last modification - * of the collected plugin files - * - * @return int - */ - public function modified(): int - { - $files = $this->files(); - $modified = [0]; + /** + * Returns the last modification + * of the collected plugin files + * + * @return int + */ + public function modified(): int + { + $files = $this->files(); + $modified = [0]; - foreach ($files as $file) { - $modified[] = F::modified($file); - } + foreach ($files as $file) { + $modified[] = F::modified($file); + } - return max($modified); - } + return max($modified); + } - /** - * Read the files from all plugins and concatenate them - * - * @param string $type - * @return string - */ - public function read(string $type): string - { - $dist = []; + /** + * Read the files from all plugins and concatenate them + * + * @param string $type + * @return string + */ + public function read(string $type): string + { + $dist = []; - foreach ($this->files() as $file) { - if (F::extension($file) === $type) { - if ($content = F::read($file)) { - if ($type === 'js') { - $content = trim($content); + foreach ($this->files() as $file) { + // filter out files with a different type + if (F::extension($file) !== $type) { + continue; + } - // make sure that each plugin is ended correctly - if (Str::endsWith($content, ';') === false) { - $content .= ';'; - } - } + // filter out empty files and files that don't exist + $content = F::read($file); + if (!$content) { + continue; + } - $dist[] = $content; - } - } - } + if ($type === 'mjs') { + // index.dev.mjs files are turned into data URIs so they + // can be imported without having to copy them to /media + // (avoids having to clean the files from /media again) + $content = F::uri($file); + } - return implode(PHP_EOL . PHP_EOL, $dist); - } + if ($type === 'js') { + // filter out all index.js files that shouldn't be loaded + // because an index.dev.mjs exists + if (F::exists(preg_replace('/\.js$/', '.dev.mjs', $file)) === true) { + continue; + } - /** - * Absolute url to the cache file - * This is used by the panel to link the plugins - * - * @param string $type - * @return string - */ - public function url(string $type): string - { - return App::instance()->url('media') . '/plugins/index.' . $type . '?' . $this->modified(); - } + $content = trim($content); + + // make sure that each plugin is ended correctly + if (Str::endsWith($content, ';') === false) { + $content .= ';'; + } + } + + $dist[] = $content; + } + + if ($type === 'mjs') { + // if no index.dev.mjs modules exist, we MUST return an empty string instead + // of loading an empty array; this is because the module loader code uses + // top level await, which is not compatible with Kirby's minimum browser + // version requirements and therefore must not appear in a default setup + if (empty($dist)) { + return ''; + } + + $modules = Json::encode($dist); + $modulePromise = "Promise.all($modules.map(url => import(url)))"; + return "try { await $modulePromise } catch (e) { console.error(e) }" . PHP_EOL; + } + + return implode(PHP_EOL . PHP_EOL, $dist); + } + + /** + * Absolute url to the cache file + * This is used by the panel to link the plugins + * + * @param string $type + * @return string + */ + public function url(string $type): string + { + return App::instance()->url('media') . '/plugins/index.' . $type . '?' . $this->modified(); + } } diff --git a/kirby/src/Panel/Redirect.php b/kirby/src/Panel/Redirect.php index 929caad..0c63a2e 100644 --- a/kirby/src/Panel/Redirect.php +++ b/kirby/src/Panel/Redirect.php @@ -18,29 +18,29 @@ use Exception; */ class Redirect extends Exception { - /** - * Returns the HTTP code for the redirect - * - * @return int - */ - public function code(): int - { - $codes = [301, 302, 303, 307, 308]; + /** + * Returns the HTTP code for the redirect + * + * @return int + */ + public function code(): int + { + $codes = [301, 302, 303, 307, 308]; - if (in_array($this->getCode(), $codes) === true) { - return $this->getCode(); - } + if (in_array($this->getCode(), $codes) === true) { + return $this->getCode(); + } - return 302; - } + return 302; + } - /** - * Returns the URL for the redirect - * - * @return string - */ - public function location(): string - { - return $this->getMessage(); - } + /** + * Returns the URL for the redirect + * + * @return string + */ + public function location(): string + { + return $this->getMessage(); + } } diff --git a/kirby/src/Panel/Search.php b/kirby/src/Panel/Search.php index ab700ad..20e75ae 100644 --- a/kirby/src/Panel/Search.php +++ b/kirby/src/Panel/Search.php @@ -16,21 +16,21 @@ namespace Kirby\Panel; */ class Search extends Json { - protected static $key = '$search'; + protected static $key = '$search'; - /** - * @param mixed $data - * @param array $options - * @return \Kirby\Http\Response - */ - public static function response($data, array $options = []) - { - if (is_array($data) === true) { - $data = [ - 'results' => $data - ]; - } + /** + * @param mixed $data + * @param array $options + * @return \Kirby\Http\Response + */ + public static function response($data, array $options = []) + { + if (is_array($data) === true) { + $data = [ + 'results' => $data + ]; + } - return parent::response($data, $options); - } + return parent::response($data, $options); + } } diff --git a/kirby/src/Panel/Site.php b/kirby/src/Panel/Site.php index 9e6be0f..92f5178 100644 --- a/kirby/src/Panel/Site.php +++ b/kirby/src/Panel/Site.php @@ -14,81 +14,86 @@ namespace Kirby\Panel; */ class Site extends Model { - /** - * Returns the setup for a dropdown option - * which is used in the changes dropdown - * for example. - * - * @return array - */ - public function dropdownOption(): array - { - return [ - 'icon' => 'home', - 'text' => $this->model->title()->value(), - ] + parent::dropdownOption(); - } + /** + * @var \Kirby\Cms\Site + */ + protected $model; - /** - * Returns the image file object based on provided query - * - * @internal - * @param string|null $query - * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null - */ - protected function imageSource(string $query = null) - { - if ($query === null) { - $query = 'site.image'; - } + /** + * Returns the setup for a dropdown option + * which is used in the changes dropdown + * for example. + * + * @return array + */ + public function dropdownOption(): array + { + return [ + 'icon' => 'home', + 'text' => $this->model->title()->value(), + ] + parent::dropdownOption(); + } - return parent::imageSource($query); - } + /** + * Returns the image file object based on provided query + * + * @internal + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null + */ + protected function imageSource(string $query = null) + { + if ($query === null) { + $query = 'site.image'; + } - /** - * Returns the full path without leading slash - * - * @return string - */ - public function path(): string - { - return 'site'; - } + return parent::imageSource($query); + } - /** - * Returns the data array for the - * view's component props - * - * @internal - * - * @return array - */ - public function props(): array - { - return array_merge(parent::props(), [ - 'blueprint' => 'site', - 'model' => [ - 'content' => $this->content(), - 'link' => $this->url(true), - 'previewUrl' => $this->model->previewUrl(), - 'title' => $this->model->title()->toString(), - ] - ]); - } + /** + * Returns the full path without leading slash + * + * @return string + */ + public function path(): string + { + return 'site'; + } - /** - * Returns the data array for - * this model's Panel view - * - * @internal - * - * @return array - */ - public function view(): array - { - return [ - 'component' => 'k-site-view', - 'props' => $this->props() - ]; - } + /** + * Returns the data array for the + * view's component props + * + * @internal + * + * @return array + */ + public function props(): array + { + return array_merge(parent::props(), [ + 'blueprint' => 'site', + 'model' => [ + 'content' => $this->content(), + 'link' => $this->url(true), + 'previewUrl' => $this->model->previewUrl(), + 'title' => $this->model->title()->toString(), + ] + ]); + } + + /** + * Returns the data array for + * this model's Panel view + * + * @internal + * + * @return array + */ + public function view(): array + { + return [ + 'component' => 'k-site-view', + 'props' => $this->props() + ]; + } } diff --git a/kirby/src/Panel/User.php b/kirby/src/Panel/User.php index cdee741..69275ef 100644 --- a/kirby/src/Panel/User.php +++ b/kirby/src/Panel/User.php @@ -2,6 +2,9 @@ namespace Kirby\Panel; +use Kirby\Cms\Url; +use Kirby\Toolkit\I18n; + /** * Provides information about the user model for the Panel * @since 3.6.0 @@ -14,259 +17,258 @@ namespace Kirby\Panel; */ class User extends Model { - /** - * Breadcrumb array - * - * @return array - */ - public function breadcrumb(): array - { - return [ - [ - 'label' => $this->model->username(), - 'link' => $this->url(true), - ] - ]; - } + /** + * @var \Kirby\Cms\User + */ + protected $model; - /** - * Provides options for the user dropdown - * - * @param array $options - * @return array - */ - public function dropdown(array $options = []): array - { - $account = $this->model->isLoggedIn(); - $i18nPrefix = $account ? 'account' : 'user'; - $permissions = $this->options(['preview']); - $url = $this->url(true); - $result = []; + /** + * Breadcrumb array + * + * @return array + */ + public function breadcrumb(): array + { + return [ + [ + 'label' => $this->model->username(), + 'link' => $this->url(true), + ] + ]; + } - $result[] = [ - 'dialog' => $url . '/changeName', - 'icon' => 'title', - 'text' => t($i18nPrefix . '.changeName'), - 'disabled' => $this->isDisabledDropdownOption('changeName', $options, $permissions) - ]; + /** + * Provides options for the user dropdown + * + * @param array $options + * @return array + */ + public function dropdown(array $options = []): array + { + $account = $this->model->isLoggedIn(); + $i18nPrefix = $account ? 'account' : 'user'; + $permissions = $this->options(['preview']); + $url = $this->url(true); + $result = []; - $result[] = '-'; + $result[] = [ + 'dialog' => $url . '/changeName', + 'icon' => 'title', + 'text' => I18n::translate($i18nPrefix . '.changeName'), + 'disabled' => $this->isDisabledDropdownOption('changeName', $options, $permissions) + ]; - $result[] = [ - 'dialog' => $url . '/changeEmail', - 'icon' => 'email', - 'text' => t('user.changeEmail'), - 'disabled' => $this->isDisabledDropdownOption('changeEmail', $options, $permissions) - ]; + $result[] = '-'; - $result[] = [ - 'dialog' => $url . '/changeRole', - 'icon' => 'bolt', - 'text' => t('user.changeRole'), - 'disabled' => $this->isDisabledDropdownOption('changeRole', $options, $permissions) - ]; + $result[] = [ + 'dialog' => $url . '/changeEmail', + 'icon' => 'email', + 'text' => I18n::translate('user.changeEmail'), + 'disabled' => $this->isDisabledDropdownOption('changeEmail', $options, $permissions) + ]; - $result[] = [ - 'dialog' => $url . '/changePassword', - 'icon' => 'key', - 'text' => t('user.changePassword'), - 'disabled' => $this->isDisabledDropdownOption('changePassword', $options, $permissions) - ]; + $result[] = [ + 'dialog' => $url . '/changeRole', + 'icon' => 'bolt', + 'text' => I18n::translate('user.changeRole'), + 'disabled' => $this->isDisabledDropdownOption('changeRole', $options, $permissions) + ]; - $result[] = [ - 'dialog' => $url . '/changeLanguage', - 'icon' => 'globe', - 'text' => t('user.changeLanguage'), - 'disabled' => $this->isDisabledDropdownOption('changeLanguage', $options, $permissions) - ]; + $result[] = [ + 'dialog' => $url . '/changePassword', + 'icon' => 'key', + 'text' => I18n::translate('user.changePassword'), + 'disabled' => $this->isDisabledDropdownOption('changePassword', $options, $permissions) + ]; - $result[] = '-'; + $result[] = [ + 'dialog' => $url . '/changeLanguage', + 'icon' => 'globe', + 'text' => I18n::translate('user.changeLanguage'), + 'disabled' => $this->isDisabledDropdownOption('changeLanguage', $options, $permissions) + ]; - $result[] = [ - 'dialog' => $url . '/delete', - 'icon' => 'trash', - 'text' => t($i18nPrefix . '.delete'), - 'disabled' => $this->isDisabledDropdownOption('delete', $options, $permissions) - ]; + $result[] = '-'; - return $result; - } + $result[] = [ + 'dialog' => $url . '/delete', + 'icon' => 'trash', + 'text' => I18n::translate($i18nPrefix . '.delete'), + 'disabled' => $this->isDisabledDropdownOption('delete', $options, $permissions) + ]; - /** - * Returns the setup for a dropdown option - * which is used in the changes dropdown - * for example. - * - * @return array - */ - public function dropdownOption(): array - { - return [ - 'icon' => 'user', - 'text' => $this->model->username(), - ] + parent::dropdownOption(); - } + return $result; + } - /** - * @return string|null - */ - public function home(): ?string - { - if ($home = ($this->model->blueprint()->home() ?? null)) { - $url = $this->model->toString($home); - return url($url); - } + /** + * Returns the setup for a dropdown option + * which is used in the changes dropdown + * for example. + * + * @return array + */ + public function dropdownOption(): array + { + return [ + 'icon' => 'user', + 'text' => $this->model->username(), + ] + parent::dropdownOption(); + } - return Panel::url('site'); - } + /** + * @return string|null + */ + public function home(): ?string + { + if ($home = ($this->model->blueprint()->home() ?? null)) { + $url = $this->model->toString($home); + return Url::to($url); + } - /** - * Default settings for the user's Panel image - * - * @return array - */ - protected function imageDefaults(): array - { - return array_merge(parent::imageDefaults(), [ - 'back' => 'black', - 'icon' => 'user', - 'ratio' => '1/1', - ]); - } + return Panel::url('site'); + } - /** - * Returns the image file object based on provided query - * - * @param string|null $query - * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null - */ - protected function imageSource(string $query = null) - { - if ($query === null) { - return $this->model->avatar(); - } + /** + * Default settings for the user's Panel image + * + * @return array + */ + protected function imageDefaults(): array + { + return array_merge(parent::imageDefaults(), [ + 'back' => 'black', + 'icon' => 'user', + 'ratio' => '1/1', + ]); + } - return parent::imageSource($query); - } + /** + * Returns the image file object based on provided query + * + * @param string|null $query + * @return \Kirby\Cms\File|\Kirby\Filesystem\Asset|null + */ + protected function imageSource(string $query = null) + { + if ($query === null) { + return $this->model->avatar(); + } - /** - * Returns the full path without leading slash - * - * @return string - */ - public function path(): string - { - // path to your own account - if ($this->model->isLoggedIn() === true) { - return 'account'; - } + return parent::imageSource($query); + } - return 'users/' . $this->model->id(); - } + /** + * Returns the full path without leading slash + * + * @return string + */ + public function path(): string + { + // path to your own account + if ($this->model->isLoggedIn() === true) { + return 'account'; + } - /** - * Returns prepared data for the panel user picker - * - * @param array|null $params - * @return array - */ - public function pickerData(array $params = null): array - { - $params['text'] ??= '{{ user.username }}'; + return 'users/' . $this->model->id(); + } - return array_merge(parent::pickerData($params), [ - 'email' => $this->model->email(), - 'username' => $this->model->username(), - ]); - } + /** + * Returns prepared data for the panel user picker + * + * @param array|null $params + * @return array + */ + public function pickerData(array $params = null): array + { + $params['text'] ??= '{{ user.username }}'; - /** - * Returns navigation array with - * previous and next user - * - * @internal - * - * @return array - */ - public function prevNext(): array - { - $user = $this->model; + return array_merge(parent::pickerData($params), [ + 'email' => $this->model->email(), + 'username' => $this->model->username(), + ]); + } - return [ - 'next' => function () use ($user) { - $next = $user->next(); - return $next ? $next->panel()->toLink('username') : null; - }, - 'prev' => function () use ($user) { - $prev = $user->prev(); - return $prev ? $prev->panel()->toLink('username') : null; - } - ]; - } + /** + * Returns navigation array with + * previous and next user + * + * @internal + * + * @return array + */ + public function prevNext(): array + { + $user = $this->model; - /** - * Returns the data array for the - * view's component props - * - * @internal - * - * @return array - */ - public function props(): array - { - $user = $this->model; - $account = $user->isLoggedIn(); - $avatar = $user->avatar(); + return [ + 'next' => fn () => $this->toPrevNextLink($user->next(), 'username'), + 'prev' => fn () => $this->toPrevNextLink($user->prev(), 'username') + ]; + } - return array_merge( - parent::props(), - $account ? [] : $this->prevNext(), - [ - 'blueprint' => $this->model->role()->name(), - 'model' => [ - 'account' => $account, - 'avatar' => $avatar ? $avatar->url() : null, - 'content' => $this->content(), - 'email' => $user->email(), - 'id' => $user->id(), - 'language' => $this->translation()->name(), - 'link' => $this->url(true), - 'name' => $user->name()->toString(), - 'role' => $user->role()->title(), - 'username' => $user->username(), - ] - ] - ); - } + /** + * Returns the data array for the + * view's component props + * + * @internal + * + * @return array + */ + public function props(): array + { + $user = $this->model; + $account = $user->isLoggedIn(); + $avatar = $user->avatar(); - /** - * Returns the Translation object - * for the selected Panel language - * - * @return \Kirby\Cms\Translation - */ - public function translation() - { - $kirby = $this->model->kirby(); - $lang = $this->model->language(); - return $kirby->translation($lang); - } + return array_merge( + parent::props(), + $account ? [] : $this->prevNext(), + [ + 'blueprint' => $this->model->role()->name(), + 'model' => [ + 'account' => $account, + 'avatar' => $avatar ? $avatar->url() : null, + 'content' => $this->content(), + 'email' => $user->email(), + 'id' => $user->id(), + 'language' => $this->translation()->name(), + 'link' => $this->url(true), + 'name' => $user->name()->toString(), + 'role' => $user->role()->title(), + 'username' => $user->username(), + ] + ] + ); + } - /** - * Returns the data array for - * this model's Panel view - * - * @internal - * - * @return array - */ - public function view(): array - { - return [ - 'breadcrumb' => $this->breadcrumb(), - 'component' => 'k-user-view', - 'props' => $this->props(), - 'title' => $this->model->username(), - ]; - } + /** + * Returns the Translation object + * for the selected Panel language + * + * @return \Kirby\Cms\Translation + */ + public function translation() + { + $kirby = $this->model->kirby(); + $lang = $this->model->language(); + return $kirby->translation($lang); + } + + /** + * Returns the data array for + * this model's Panel view + * + * @internal + * + * @return array + */ + public function view(): array + { + return [ + 'breadcrumb' => $this->breadcrumb(), + 'component' => 'k-user-view', + 'props' => $this->props(), + 'title' => $this->model->username(), + ]; + } } diff --git a/kirby/src/Panel/View.php b/kirby/src/Panel/View.php index 87ae814..3eb7155 100644 --- a/kirby/src/Panel/View.php +++ b/kirby/src/Panel/View.php @@ -2,9 +2,10 @@ namespace Kirby\Panel; +use Kirby\Cms\App; use Kirby\Http\Response; -use Kirby\Http\Url; use Kirby\Toolkit\A; +use Kirby\Toolkit\I18n; use Kirby\Toolkit\Str; /** @@ -21,435 +22,434 @@ use Kirby\Toolkit\Str; */ class View { - /** - * Filters the data array based on headers or - * query parameters. Requests can return only - * certain data fields that way or globals can - * be injected on demand. - * - * @param array $data - * @return array - */ - public static function apply(array $data): array - { - $request = kirby()->request(); - $only = $request->header('X-Fiber-Only') ?? get('_only'); + /** + * Filters the data array based on headers or + * query parameters. Requests can return only + * certain data fields that way or globals can + * be injected on demand. + * + * @param array $data + * @return array + */ + public static function apply(array $data): array + { + $request = App::instance()->request(); + $only = $request->header('X-Fiber-Only') ?? $request->get('_only'); - if (empty($only) === false) { - return static::applyOnly($data, $only); - } + if (empty($only) === false) { + return static::applyOnly($data, $only); + } - $globals = $request->header('X-Fiber-Globals') ?? get('_globals'); + $globals = $request->header('X-Fiber-Globals') ?? $request->get('_globals'); - if (empty($globals) === false) { - return static::applyGlobals($data, $globals); - } + if (empty($globals) === false) { + return static::applyGlobals($data, $globals); + } - return A::apply($data); - } + return A::apply($data); + } - /** - * Checks if globals should be included in a JSON Fiber request. They are normally - * only loaded with the full document request, but sometimes need to be updated. - * - * A global request can be activated with the `X-Fiber-Globals` header or the - * `_globals` query parameter. - * - * @param array $data - * @param string|null $globals - * @return array - */ - public static function applyGlobals(array $data, ?string $globals = null): array - { - // split globals string into an array of fields - $globalKeys = Str::split($globals, ','); + /** + * Checks if globals should be included in a JSON Fiber request. They are normally + * only loaded with the full document request, but sometimes need to be updated. + * + * A global request can be activated with the `X-Fiber-Globals` header or the + * `_globals` query parameter. + * + * @param array $data + * @param string|null $globals + * @return array + */ + public static function applyGlobals(array $data, ?string $globals = null): array + { + // split globals string into an array of fields + $globalKeys = Str::split($globals, ','); - // add requested globals - if (empty($globalKeys) === true) { - return $data; - } + // add requested globals + if (empty($globalKeys) === true) { + return $data; + } - $globals = static::globals(); + $globals = static::globals(); - foreach ($globalKeys as $globalKey) { - if (isset($globals[$globalKey]) === true) { - $data[$globalKey] = $globals[$globalKey]; - } - } + foreach ($globalKeys as $globalKey) { + if (isset($globals[$globalKey]) === true) { + $data[$globalKey] = $globals[$globalKey]; + } + } - // merge with shared data - return A::apply($data); - } + // merge with shared data + return A::apply($data); + } - /** - * Checks if the request should only return a limited - * set of data. This can be activated with the `X-Fiber-Only` - * header or the `_only` query parameter in a request. - * - * Such requests can fetch shared data or globals. - * Globals will be loaded on demand. - * - * @param array $data - * @param string|null $only - * @return array - */ - public static function applyOnly(array $data, ?string $only = null): array - { - // split include string into an array of fields - $onlyKeys = Str::split($only, ','); + /** + * Checks if the request should only return a limited + * set of data. This can be activated with the `X-Fiber-Only` + * header or the `_only` query parameter in a request. + * + * Such requests can fetch shared data or globals. + * Globals will be loaded on demand. + * + * @param array $data + * @param string|null $only + * @return array + */ + public static function applyOnly(array $data, ?string $only = null): array + { + // split include string into an array of fields + $onlyKeys = Str::split($only, ','); - // if a full request is made, return all data - if (empty($onlyKeys) === true) { - return $data; - } + // if a full request is made, return all data + if (empty($onlyKeys) === true) { + return $data; + } - // otherwise filter data based on - // dot notation, e.g. `$props.tab.columns` - $result = []; + // otherwise filter data based on + // dot notation, e.g. `$props.tab.columns` + $result = []; - // check if globals are requested and need to be merged - if (Str::contains($only, '$')) { - $data = array_merge_recursive(static::globals(), $data); - } + // check if globals are requested and need to be merged + if (Str::contains($only, '$')) { + $data = array_merge_recursive(static::globals(), $data); + } - // make sure the data is already resolved to make - // nested data fetching work - $data = A::apply($data); + // make sure the data is already resolved to make + // nested data fetching work + $data = A::apply($data); - // build a new array with all requested data - foreach ($onlyKeys as $onlyKey) { - $result[$onlyKey] = A::get($data, $onlyKey); - } + // build a new array with all requested data + foreach ($onlyKeys as $onlyKey) { + $result[$onlyKey] = A::get($data, $onlyKey); + } - // Nest dotted keys in array but ignore $translation - return A::nest($result, [ - '$translation' - ]); - } + // Nest dotted keys in array but ignore $translation + return A::nest($result, [ + '$translation' + ]); + } - /** - * Creates the shared data array for the individual views - * The full shared data is always sent on every JSON and - * full document request unless the `X-Fiber-Only` header or - * the `_only` query parameter is set. - * - * @param array $view - * @param array $options - * @return array - */ - public static function data(array $view = [], array $options = []): array - { - $kirby = kirby(); + /** + * Creates the shared data array for the individual views + * The full shared data is always sent on every JSON and + * full document request unless the `X-Fiber-Only` header or + * the `_only` query parameter is set. + * + * @param array $view + * @param array $options + * @return array + */ + public static function data(array $view = [], array $options = []): array + { + $kirby = App::instance(); - // multilang setup check - $multilang = Panel::multilang(); + // multilang setup check + $multilang = Panel::multilang(); - // get the authenticated user - $user = $kirby->user(); + // get the authenticated user + $user = $kirby->user(); - // user permissions - $permissions = $user ? $user->role()->permissions()->toArray() : []; + // user permissions + $permissions = $user ? $user->role()->permissions()->toArray() : []; - // current content language - $language = $kirby->language(); + // current content language + $language = $kirby->language(); - // shared data for all requests - return [ - '$direction' => function () use ($kirby, $multilang, $language, $user) { - if ($multilang === true && $language && $user) { - $isDefault = $language->direction() === $kirby->defaultLanguage()->direction(); - $isFromUser = $language->code() === $user->language(); + // shared data for all requests + return [ + '$direction' => function () use ($kirby, $multilang, $language, $user) { + if ($multilang === true && $language && $user) { + $isDefault = $language->direction() === $kirby->defaultLanguage()->direction(); + $isFromUser = $language->code() === $user->language(); - if ($isDefault === false && $isFromUser === false) { - return $language->direction(); - } - } - }, - '$language' => function () use ($kirby, $multilang, $language) { - if ($multilang === true && $language) { - return [ - 'code' => $language->code(), - 'default' => $language->isDefault(), - 'direction' => $language->direction(), - 'name' => $language->name(), - 'rules' => $language->rules(), - ]; - } - }, - '$languages' => function () use ($kirby, $multilang): array { - if ($multilang === true) { - return $kirby->languages()->values(fn ($language) => [ - 'code' => $language->code(), - 'default' => $language->isDefault(), - 'direction' => $language->direction(), - 'name' => $language->name(), - 'rules' => $language->rules(), - ]); - } + if ($isDefault === false && $isFromUser === false) { + return $language->direction(); + } + } + }, + '$language' => function () use ($kirby, $multilang, $language) { + if ($multilang === true && $language) { + return [ + 'code' => $language->code(), + 'default' => $language->isDefault(), + 'direction' => $language->direction(), + 'name' => $language->name(), + 'rules' => $language->rules(), + ]; + } + }, + '$languages' => function () use ($kirby, $multilang): array { + if ($multilang === true) { + return $kirby->languages()->values(fn ($language) => [ + 'code' => $language->code(), + 'default' => $language->isDefault(), + 'direction' => $language->direction(), + 'name' => $language->name(), + 'rules' => $language->rules(), + ]); + } - return []; - }, - '$menu' => function () use ($options, $permissions) { - return static::menu($options['areas'] ?? [], $permissions, $options['area']['id'] ?? null); - }, - '$permissions' => $permissions, - '$license' => (bool)$kirby->system()->license(), - '$multilang' => $multilang, - '$searches' => static::searches($options['areas'] ?? [], $permissions), - '$url' => Url::current(), - '$user' => function () use ($user) { - if ($user) { - return [ - 'email' => $user->email(), - 'id' => $user->id(), - 'language' => $user->language(), - 'role' => $user->role()->id(), - 'username' => $user->username(), - ]; - } + return []; + }, + '$menu' => function () use ($options, $permissions) { + return static::menu($options['areas'] ?? [], $permissions, $options['area']['id'] ?? null); + }, + '$permissions' => $permissions, + '$license' => (bool)$kirby->system()->license(), + '$multilang' => $multilang, + '$searches' => static::searches($options['areas'] ?? [], $permissions), + '$url' => $kirby->request()->url()->toString(), + '$user' => function () use ($user) { + if ($user) { + return [ + 'email' => $user->email(), + 'id' => $user->id(), + 'language' => $user->language(), + 'role' => $user->role()->id(), + 'username' => $user->username(), + ]; + } - return null; - }, - '$view' => function () use ($kirby, $options, $view) { - $defaults = [ - 'breadcrumb' => [], - 'code' => 200, - 'path' => Str::after($kirby->path(), '/'), - 'timestamp' => (int)(microtime(true) * 1000), - 'props' => [], - 'search' => $kirby->option('panel.search.type', 'pages') - ]; + return null; + }, + '$view' => function () use ($kirby, $options, $view) { + $defaults = [ + 'breadcrumb' => [], + 'code' => 200, + 'path' => Str::after($kirby->path(), '/'), + 'timestamp' => (int)(microtime(true) * 1000), + 'props' => [], + 'search' => $kirby->option('panel.search.type', 'pages') + ]; - $view = array_replace_recursive($defaults, $options['area'] ?? [], $view); + $view = array_replace_recursive($defaults, $options['area'] ?? [], $view); - // make sure that views and dialogs are gone - unset( - $view['dialogs'], - $view['dropdowns'], - $view['searches'], - $view['views'] - ); + // make sure that views and dialogs are gone + unset( + $view['dialogs'], + $view['dropdowns'], + $view['searches'], + $view['views'] + ); - // resolve all callbacks in the view array - return A::apply($view); - } - ]; - } + // resolve all callbacks in the view array + return A::apply($view); + } + ]; + } - /** - * Renders the error view with provided message - * - * @param string $message - * @param int $code - * @return array - */ - public static function error(string $message, int $code = 404) - { - return [ - 'code' => $code, - 'component' => 'k-error-view', - 'error' => $message, - 'props' => [ - 'error' => $message, - 'layout' => Panel::hasAccess(kirby()->user()) ? 'inside' : 'outside' - ], - 'title' => 'Error' - ]; - } + /** + * Renders the error view with provided message + * + * @param string $message + * @param int $code + * @return array + */ + public static function error(string $message, int $code = 404) + { + return [ + 'code' => $code, + 'component' => 'k-error-view', + 'error' => $message, + 'props' => [ + 'error' => $message, + 'layout' => Panel::hasAccess(App::instance()->user()) ? 'inside' : 'outside' + ], + 'title' => 'Error' + ]; + } - /** - * Creates global data for the Panel. - * This will be injected in the full Panel - * view via the script tag. Global data - * is only requested once on the first page load. - * It can be loaded partially later if needed, - * but is otherwise not included in Fiber calls. - * - * @return array - */ - public static function globals(): array - { - $kirby = kirby(); + /** + * Creates global data for the Panel. + * This will be injected in the full Panel + * view via the script tag. Global data + * is only requested once on the first page load. + * It can be loaded partially later if needed, + * but is otherwise not included in Fiber calls. + * + * @return array + */ + public static function globals(): array + { + $kirby = App::instance(); - return [ - '$config' => function () use ($kirby) { - return [ - 'debug' => $kirby->option('debug', false), - 'kirbytext' => $kirby->option('panel.kirbytext', true), - 'search' => [ - 'limit' => $kirby->option('panel.search.limit', 10), - 'type' => $kirby->option('panel.search.type', 'pages') - ], - 'translation' => $kirby->option('panel.language', 'en'), - ]; - }, - '$system' => function () use ($kirby) { - $locales = []; + return [ + '$config' => function () use ($kirby) { + return [ + 'debug' => $kirby->option('debug', false), + 'kirbytext' => $kirby->option('panel.kirbytext', true), + 'search' => [ + 'limit' => $kirby->option('panel.search.limit', 10), + 'type' => $kirby->option('panel.search.type', 'pages') + ], + 'translation' => $kirby->option('panel.language', 'en'), + ]; + }, + '$system' => function () use ($kirby) { + $locales = []; - foreach ($kirby->translations() as $translation) { - $locales[$translation->code()] = $translation->locale(); - } + foreach ($kirby->translations() as $translation) { + $locales[$translation->code()] = $translation->locale(); + } - return [ - 'ascii' => Str::$ascii, - 'csrf' => $kirby->auth()->csrfFromSession(), - 'isLocal' => $kirby->system()->isLocal(), - 'locales' => $locales, - 'slugs' => Str::$language, - 'title' => $kirby->site()->title()->or('Kirby Panel')->toString() - ]; - }, - '$translation' => function () use ($kirby) { - if ($user = $kirby->user()) { - $translation = $kirby->translation($user->language()); - } else { - $translation = $kirby->translation($kirby->panelLanguage()); - } + return [ + 'ascii' => Str::$ascii, + 'csrf' => $kirby->auth()->csrfFromSession(), + 'isLocal' => $kirby->system()->isLocal(), + 'locales' => $locales, + 'slugs' => Str::$language, + 'title' => $kirby->site()->title()->or('Kirby Panel')->toString() + ]; + }, + '$translation' => function () use ($kirby) { + if ($user = $kirby->user()) { + $translation = $kirby->translation($user->language()); + } else { + $translation = $kirby->translation($kirby->panelLanguage()); + } - return [ - 'code' => $translation->code(), - 'data' => $translation->dataWithFallback(), - 'direction' => $translation->direction(), - 'name' => $translation->name(), - ]; - }, - '$urls' => fn () => [ - 'api' => $kirby->url('api'), - 'site' => $kirby->url('index') - ] - ]; - } + return [ + 'code' => $translation->code(), + 'data' => $translation->dataWithFallback(), + 'direction' => $translation->direction(), + 'name' => $translation->name(), + ]; + }, + '$urls' => fn () => [ + 'api' => $kirby->url('api'), + 'site' => $kirby->url('index') + ] + ]; + } - /** - * Creates the menu for the topbar - * - * @param array $areas - * @param array $permissions - * @param string|null $current - * @return array - */ - public static function menu(?array $areas = [], ?array $permissions = [], ?string $current = null): array - { - $menu = []; + /** + * Creates the menu for the topbar + * + * @param array $areas + * @param array $permissions + * @param string|null $current + * @return array + */ + public static function menu(?array $areas = [], ?array $permissions = [], ?string $current = null): array + { + $menu = []; - // areas - foreach ($areas as $areaId => $area) { - $access = $permissions['access'][$areaId] ?? true; + // areas + foreach ($areas as $areaId => $area) { + $access = $permissions['access'][$areaId] ?? true; - // areas without access permissions get skipped entirely - if ($access === false) { - continue; - } + // areas without access permissions get skipped entirely + if ($access === false) { + continue; + } - // fetch custom menu settings from the area definition - $menuSetting = $area['menu'] ?? false; + // fetch custom menu settings from the area definition + $menuSetting = $area['menu'] ?? false; - // menu settings can be a callback that can return true, false or disabled - if (is_a($menuSetting, 'Closure') === true) { - $menuSetting = $menuSetting($areas, $permissions, $current); - } + // menu settings can be a callback that can return true, false or disabled + if (is_a($menuSetting, 'Closure') === true) { + $menuSetting = $menuSetting($areas, $permissions, $current); + } - // false will remove the area entirely just like with - // disabled permissions - if ($menuSetting === false) { - continue; - } + // false will remove the area entirely just like with + // disabled permissions + if ($menuSetting === false) { + continue; + } - $menu[] = [ - 'current' => $areaId === $current, - 'disabled' => $menuSetting === 'disabled', - 'icon' => $area['icon'], - 'id' => $areaId, - 'link' => $area['link'], - 'text' => $area['label'], - ]; - } + $menu[] = [ + 'current' => $areaId === $current, + 'disabled' => $menuSetting === 'disabled', + 'icon' => $area['icon'], + 'id' => $areaId, + 'link' => $area['link'], + 'text' => $area['label'], + ]; + } - $menu[] = '-'; - $menu[] = [ - 'current' => $current === 'account', - 'icon' => 'account', - 'id' => 'account', - 'link' => 'account', - 'disabled' => ($permissions['access']['account'] ?? false) === false, - 'text' => t('view.account'), - ]; - $menu[] = '-'; + $menu[] = '-'; + $menu[] = [ + 'current' => $current === 'account', + 'icon' => 'account', + 'id' => 'account', + 'link' => 'account', + 'disabled' => ($permissions['access']['account'] ?? false) === false, + 'text' => I18n::translate('view.account'), + ]; + $menu[] = '-'; - // logout - $menu[] = [ - 'icon' => 'logout', - 'id' => 'logout', - 'link' => 'logout', - 'text' => t('logout') - ]; - return $menu; - } + // logout + $menu[] = [ + 'icon' => 'logout', + 'id' => 'logout', + 'link' => 'logout', + 'text' => I18n::translate('logout') + ]; + return $menu; + } - /** - * Renders the main panel view either as - * JSON response or full HTML document based - * on the request header or query params - * - * @param mixed $data - * @param array $options - * @return \Kirby\Http\Response - */ - public static function response($data, array $options = []) - { - // handle redirects - if (is_a($data, 'Kirby\Panel\Redirect') === true) { - return Response::redirect($data->location(), $data->code()); + /** + * Renders the main panel view either as + * JSON response or full HTML document based + * on the request header or query params + * + * @param mixed $data + * @param array $options + * @return \Kirby\Http\Response + */ + public static function response($data, array $options = []) + { + // handle redirects + if (is_a($data, 'Kirby\Panel\Redirect') === true) { + return Response::redirect($data->location(), $data->code()); - // handle Kirby exceptions - } elseif (is_a($data, 'Kirby\Exception\Exception') === true) { - $data = static::error($data->getMessage(), $data->getHttpCode()); + // handle Kirby exceptions + } elseif (is_a($data, 'Kirby\Exception\Exception') === true) { + $data = static::error($data->getMessage(), $data->getHttpCode()); - // handle regular exceptions - } elseif (is_a($data, 'Throwable') === true) { - $data = static::error($data->getMessage(), 500); + // handle regular exceptions + } elseif (is_a($data, 'Throwable') === true) { + $data = static::error($data->getMessage(), 500); - // only expect arrays from here on - } elseif (is_array($data) === false) { - $data = static::error('Invalid Panel response', 500); - } + // only expect arrays from here on + } elseif (is_array($data) === false) { + $data = static::error('Invalid Panel response', 500); + } - // get all data for the request - $fiber = static::data($data, $options); + // get all data for the request + $fiber = static::data($data, $options); - // if requested, send $fiber data as JSON - if (Panel::isFiberRequest() === true) { + // if requested, send $fiber data as JSON + if (Panel::isFiberRequest() === true) { + // filter data, if only or globals headers or + // query parameters are set + $fiber = static::apply($fiber); - // filter data, if only or globals headers or - // query parameters are set - $fiber = static::apply($fiber); + return Panel::json($fiber, $fiber['$view']['code'] ?? 200); + } - return Panel::json($fiber, $fiber['$view']['code'] ?? 200); - } + // load globals for the full document response + $globals = static::globals(); - // load globals for the full document response - $globals = static::globals(); + // resolve and merge globals and shared data + $fiber = array_merge_recursive(A::apply($globals), A::apply($fiber)); - // resolve and merge globals and shared data - $fiber = array_merge_recursive(A::apply($globals), A::apply($fiber)); + // render the full HTML document + return Document::response($fiber); + } - // render the full HTML document - return Document::response($fiber); - } + public static function searches(array $areas, array $permissions) + { + $searches = []; - public static function searches(array $areas, array $permissions) - { - $searches = []; - - foreach ($areas as $area) { - foreach ($area['searches'] ?? [] as $id => $params) { - $searches[$id] = [ - 'icon' => $params['icon'] ?? 'search', - 'label' => $params['label'] ?? Str::ucfirst($id), - 'id' => $id - ]; - } - } - return $searches; - } + foreach ($areas as $area) { + foreach ($area['searches'] ?? [] as $id => $params) { + $searches[$id] = [ + 'icon' => $params['icon'] ?? 'search', + 'label' => $params['label'] ?? Str::ucfirst($id), + 'id' => $id + ]; + } + } + return $searches; + } } diff --git a/kirby/src/Parsley/Element.php b/kirby/src/Parsley/Element.php index 519b442..292a55a 100644 --- a/kirby/src/Parsley/Element.php +++ b/kirby/src/Parsley/Element.php @@ -21,179 +21,179 @@ use Kirby\Toolkit\Str; */ class Element { - /** - * @var array - */ - protected $marks; + /** + * @var array + */ + protected $marks; - /** - * @var \DOMElement - */ - protected $node; + /** + * @var \DOMElement + */ + protected $node; - /** - * @param \DOMElement $node - * @param array $marks - */ - public function __construct(DOMElement $node, array $marks = []) - { - $this->marks = $marks; - $this->node = $node; - } + /** + * @param \DOMElement $node + * @param array $marks + */ + public function __construct(DOMElement $node, array $marks = []) + { + $this->marks = $marks; + $this->node = $node; + } - /** - * The returns the attribute value or - * the given fallback if the attribute does not exist - * - * @param string $attr - * @param string|null $fallback - * @return string|null - */ - public function attr(string $attr, string $fallback = null): ?string - { - if ($this->node->hasAttribute($attr)) { - return $this->node->getAttribute($attr) ?? $fallback; - } + /** + * The returns the attribute value or + * the given fallback if the attribute does not exist + * + * @param string $attr + * @param string|null $fallback + * @return string|null + */ + public function attr(string $attr, string $fallback = null): ?string + { + if ($this->node->hasAttribute($attr)) { + return $this->node->getAttribute($attr) ?? $fallback; + } - return $fallback; - } + return $fallback; + } - /** - * Returns a list of all child elements - * - * @return \DOMNodeList - */ - public function children(): DOMNodeList - { - return $this->node->childNodes; - } + /** + * Returns a list of all child elements + * + * @return \DOMNodeList + */ + public function children(): DOMNodeList + { + return $this->node->childNodes; + } - /** - * Returns an array with all class names - * - * @return array - */ - public function classList(): array - { - return Str::split($this->className(), ' '); - } + /** + * Returns an array with all class names + * + * @return array + */ + public function classList(): array + { + return Str::split($this->className(), ' '); + } - /** - * Returns the value of the class attribute - * - * @return string|null - */ - public function className(): ?string - { - return $this->attr('class'); - } + /** + * Returns the value of the class attribute + * + * @return string|null + */ + public function className(): ?string + { + return $this->attr('class'); + } - /** - * Returns the original dom element - * - * @return \DOMElement - */ - public function element() - { - return $this->node; - } + /** + * Returns the original dom element + * + * @return \DOMElement + */ + public function element() + { + return $this->node; + } - /** - * Returns an array with all nested elements - * that could be found for the given query - * - * @param string $query - * @return array - */ - public function filter(string $query): array - { - $result = []; + /** + * Returns an array with all nested elements + * that could be found for the given query + * + * @param string $query + * @return array + */ + public function filter(string $query): array + { + $result = []; - if ($queryResult = $this->query($query)) { - foreach ($queryResult as $node) { - $result[] = new static($node); - } - } + if ($queryResult = $this->query($query)) { + foreach ($queryResult as $node) { + $result[] = new static($node); + } + } - return $result; - } + return $result; + } - /** - * Tries to find a single nested element by - * query and otherwise returns null - * - * @param string $query - * @return \Kirby\Parsley\Element|null - */ - public function find(string $query) - { - if ($result = $this->query($query)[0]) { - return new static($result); - } + /** + * Tries to find a single nested element by + * query and otherwise returns null + * + * @param string $query + * @return \Kirby\Parsley\Element|null + */ + public function find(string $query) + { + if ($result = $this->query($query)[0]) { + return new static($result); + } - return null; - } + return null; + } - /** - * Returns the inner HTML of the element - * - * @param array|null $marks List of allowed marks - * @return string - */ - public function innerHtml(array $marks = null): string - { - return (new Inline($this->node, $marks ?? $this->marks))->innerHtml(); - } + /** + * Returns the inner HTML of the element + * + * @param array|null $marks List of allowed marks + * @return string + */ + public function innerHtml(array $marks = null): string + { + return (new Inline($this->node, $marks ?? $this->marks))->innerHtml(); + } - /** - * Returns the contents as plain text - * - * @return string - */ - public function innerText(): string - { - return trim($this->node->textContent); - } + /** + * Returns the contents as plain text + * + * @return string + */ + public function innerText(): string + { + return trim($this->node->textContent); + } - /** - * Returns the full HTML for the element - * - * @param array|null $marks - * @return string - */ - public function outerHtml(array $marks = null): string - { - return $this->node->ownerDocument->saveHtml($this->node); - } + /** + * Returns the full HTML for the element + * + * @param array|null $marks + * @return string + */ + public function outerHtml(array $marks = null): string + { + return $this->node->ownerDocument->saveHtml($this->node); + } - /** - * Searches nested elements - * - * @param string $query - * @return DOMNodeList|null - */ - public function query(string $query) - { - return (new DOMXPath($this->node->ownerDocument))->query($query, $this->node); - } + /** + * Searches nested elements + * + * @param string $query + * @return DOMNodeList|null + */ + public function query(string $query) + { + return (new DOMXPath($this->node->ownerDocument))->query($query, $this->node); + } - /** - * Removes the element from the DOM - * - * @return void - */ - public function remove() - { - $this->node->parentNode->removeChild($this->node); - } + /** + * Removes the element from the DOM + * + * @return void + */ + public function remove() + { + $this->node->parentNode->removeChild($this->node); + } - /** - * Returns the name of the element - * - * @return string - */ - public function tagName(): string - { - return $this->node->tagName; - } + /** + * Returns the name of the element + * + * @return string + */ + public function tagName(): string + { + return $this->node->tagName; + } } diff --git a/kirby/src/Parsley/Inline.php b/kirby/src/Parsley/Inline.php index eea71f1..97c044f 100644 --- a/kirby/src/Parsley/Inline.php +++ b/kirby/src/Parsley/Inline.php @@ -20,156 +20,156 @@ use Kirby\Toolkit\Html; */ class Inline { - /** - * @var string - */ - protected $html = ''; + /** + * @var string + */ + protected $html = ''; - /** - * @var array - */ - protected $marks = []; + /** + * @var array + */ + protected $marks = []; - /** - * @param \DOMNode $node - * @param array $marks - */ - public function __construct(DOMNode $node, array $marks = []) - { - $this->createMarkRules($marks); - $this->html = trim(static::parseNode($node, $this->marks)); - } + /** + * @param \DOMNode $node + * @param array $marks + */ + public function __construct(DOMNode $node, array $marks = []) + { + $this->createMarkRules($marks); + $this->html = trim(static::parseNode($node, $this->marks) ?? ''); + } - /** - * Loads all mark rules - * - * @param array $marks - * @return array - */ - protected function createMarkRules(array $marks) - { - foreach ($marks as $mark) { - $this->marks[$mark['tag']] = $mark; - } + /** + * Loads all mark rules + * + * @param array $marks + * @return array + */ + protected function createMarkRules(array $marks) + { + foreach ($marks as $mark) { + $this->marks[$mark['tag']] = $mark; + } - return $this->marks; - } + return $this->marks; + } - /** - * Get all allowed attributes for a DOMNode - * as clean array - * - * @param DOMNode $node - * @param array $marks - * @return array - */ - public static function parseAttrs(DOMNode $node, array $marks = []): array - { - $attrs = []; - $mark = $marks[$node->tagName]; - $defaults = $mark['defaults'] ?? []; + /** + * Get all allowed attributes for a DOMNode + * as clean array + * + * @param DOMNode $node + * @param array $marks + * @return array + */ + public static function parseAttrs(DOMNode $node, array $marks = []): array + { + $attrs = []; + $mark = $marks[$node->tagName]; + $defaults = $mark['defaults'] ?? []; - foreach ($mark['attrs'] ?? [] as $attr) { - if ($node->hasAttribute($attr)) { - $attrs[$attr] = $node->getAttribute($attr); - } else { - $attrs[$attr] = $defaults[$attr] ?? null; - } - } + foreach ($mark['attrs'] ?? [] as $attr) { + if ($node->hasAttribute($attr)) { + $attrs[$attr] = $node->getAttribute($attr); + } else { + $attrs[$attr] = $defaults[$attr] ?? null; + } + } - return $attrs; - } + return $attrs; + } - /** - * Parses all children and creates clean HTML - * for each of them. - * - * @param \DOMNodeList $children - * @param array $marks - * @return string - */ - public static function parseChildren(DOMNodeList $children, array $marks): string - { - $html = ''; - foreach ($children as $child) { - $html .= static::parseNode($child, $marks); - } - return $html; - } + /** + * Parses all children and creates clean HTML + * for each of them. + * + * @param \DOMNodeList $children + * @param array $marks + * @return string + */ + public static function parseChildren(DOMNodeList $children, array $marks): string + { + $html = ''; + foreach ($children as $child) { + $html .= static::parseNode($child, $marks); + } + return $html; + } - /** - * Go through all child elements and create - * clean inner HTML for them - * - * @param DOMNode $node - * @return string|null - */ - public static function parseInnerHtml(DOMNode $node, array $marks = []): ?string - { - $html = static::parseChildren($node->childNodes, $marks); + /** + * Go through all child elements and create + * clean inner HTML for them + * + * @param DOMNode $node + * @return string|null + */ + public static function parseInnerHtml(DOMNode $node, array $marks = []): ?string + { + $html = static::parseChildren($node->childNodes, $marks); - // trim the inner HTML for paragraphs - if ($node->tagName === 'p') { - $html = trim($html); - } + // trim the inner HTML for paragraphs + if ($node->tagName === 'p') { + $html = trim($html); + } - // return null for empty inner HTML - if ($html === '') { - return null; - } + // return null for empty inner HTML + if ($html === '') { + return null; + } - return $html; - } + return $html; + } - /** - * Converts the given node to clean HTML - * - * @param \DOMNode $node - * @param array $marks - * @return string|null - */ - public static function parseNode(DOMNode $node, array $marks = []): ?string - { - if (is_a($node, 'DOMText') === true) { - return Html::encode($node->textContent); - } + /** + * Converts the given node to clean HTML + * + * @param \DOMNode $node + * @param array $marks + * @return string|null + */ + public static function parseNode(DOMNode $node, array $marks = []): ?string + { + if (is_a($node, 'DOMText') === true) { + return Html::encode($node->textContent); + } - // ignore comments - if (is_a($node, 'DOMComment') === true) { - return null; - } + // ignore comments + if (is_a($node, 'DOMComment') === true) { + return null; + } - // unknown marks - if (array_key_exists($node->tagName, $marks) === false) { - return static::parseChildren($node->childNodes, $marks); - } + // unknown marks + if (array_key_exists($node->tagName, $marks) === false) { + return static::parseChildren($node->childNodes, $marks); + } - // collect all allowed attributes - $attrs = static::parseAttrs($node, $marks); + // collect all allowed attributes + $attrs = static::parseAttrs($node, $marks); - // close self-closing elements - if (Html::isVoid($node->tagName) === true) { - return '<' . $node->tagName . attr($attrs, ' ') . ' />'; - } + // close self-closing elements + if (Html::isVoid($node->tagName) === true) { + return '<' . $node->tagName . Html::attr($attrs, null, ' ') . ' />'; + } - $innerHtml = static::parseInnerHtml($node, $marks); + $innerHtml = static::parseInnerHtml($node, $marks); - // skip empty paragraphs - if ($innerHtml === null && $node->tagName === 'p') { - return null; - } + // skip empty paragraphs + if ($innerHtml === null && $node->tagName === 'p') { + return null; + } - // create the outer html for the element - return '<' . $node->tagName . attr($attrs, ' ') . '>' . $innerHtml . 'tagName . '>'; - } + // create the outer html for the element + return '<' . $node->tagName . Html::attr($attrs, null, ' ') . '>' . $innerHtml . 'tagName . '>'; + } - /** - * Returns the HTML contents of the element - * - * @return string - */ - public function innerHtml(): string - { - return $this->html; - } + /** + * Returns the HTML contents of the element + * + * @return string + */ + public function innerHtml(): string + { + return $this->html; + } } diff --git a/kirby/src/Parsley/Parsley.php b/kirby/src/Parsley/Parsley.php index 62a1e50..d99d45e 100644 --- a/kirby/src/Parsley/Parsley.php +++ b/kirby/src/Parsley/Parsley.php @@ -20,334 +20,334 @@ use Kirby\Toolkit\Dom; */ class Parsley { - /** - * @var array - */ - protected $blocks = []; + /** + * @var array + */ + protected $blocks = []; - /** - * @var \DOMDocument - */ - protected $doc; + /** + * @var \DOMDocument + */ + protected $doc; - /** - * @var \Kirby\Toolkit\Dom - */ - protected $dom; + /** + * @var \Kirby\Toolkit\Dom + */ + protected $dom; - /** - * @var array - */ - protected $inline = []; + /** + * @var array + */ + protected $inline = []; - /** - * @var array - */ - protected $marks = []; + /** + * @var array + */ + protected $marks = []; - /** - * @var array - */ - protected $nodes = []; + /** + * @var array + */ + protected $nodes = []; - /** - * @var \Kirby\Parsley\Schema - */ - protected $schema; + /** + * @var \Kirby\Parsley\Schema + */ + protected $schema; - /** - * @var array - */ - protected $skip = []; + /** + * @var array + */ + protected $skip = []; - /** - * @var bool - */ - public static $useXmlExtension = true; + /** + * @var bool + */ + public static $useXmlExtension = true; - /** - * @param string $html - * @param \Kirby\Parsley\Schema|null $schema - */ - public function __construct(string $html, Schema $schema = null) - { - // fail gracefully if the XML extension is not installed - // or should be skipped - if ($this->useXmlExtension() === false) { - $this->blocks[] = [ - 'type' => 'markdown', - 'content' => [ - 'text' => $html, - ] - ]; - return; - } + /** + * @param string $html + * @param \Kirby\Parsley\Schema|null $schema + */ + public function __construct(string $html, Schema $schema = null) + { + // fail gracefully if the XML extension is not installed + // or should be skipped + if ($this->useXmlExtension() === false) { + $this->blocks[] = [ + 'type' => 'markdown', + 'content' => [ + 'text' => $html, + ] + ]; + return; + } - if (!preg_match('//', $html)) { - $html = '
    ' . $html . '
    '; - } + if (!preg_match('//', $html)) { + $html = '
    ' . $html . '
    '; + } - $this->dom = new Dom($html); - $this->doc = $this->dom->document(); - $this->schema = $schema ?? new Plain(); - $this->skip = $this->schema->skip(); - $this->marks = $this->schema->marks(); - $this->inline = []; + $this->dom = new Dom($html); + $this->doc = $this->dom->document(); + $this->schema = $schema ?? new Plain(); + $this->skip = $this->schema->skip(); + $this->marks = $this->schema->marks(); + $this->inline = []; - // load all allowed nodes from the schema - $this->createNodeRules($this->schema->nodes()); + // load all allowed nodes from the schema + $this->createNodeRules($this->schema->nodes()); - // start parsing at the top level and go through - // all children of the document - foreach ($this->doc->childNodes as $childNode) { - $this->parseNode($childNode); - } + // start parsing at the top level and go through + // all children of the document + foreach ($this->doc->childNodes as $childNode) { + $this->parseNode($childNode); + } - // needs to be called at last to fetch remaining - // inline elements after parsing has ended - $this->endInlineBlock(); - } + // needs to be called at last to fetch remaining + // inline elements after parsing has ended + $this->endInlineBlock(); + } - /** - * Returns all detected blocks - * - * @return array - */ - public function blocks(): array - { - return $this->blocks; - } + /** + * Returns all detected blocks + * + * @return array + */ + public function blocks(): array + { + return $this->blocks; + } - /** - * Load all node rules from the schema - * - * @param array $nodes - * @return array - */ - public function createNodeRules(array $nodes): array - { - foreach ($nodes as $node) { - $this->nodes[$node['tag']] = $node; - } + /** + * Load all node rules from the schema + * + * @param array $nodes + * @return array + */ + public function createNodeRules(array $nodes): array + { + foreach ($nodes as $node) { + $this->nodes[$node['tag']] = $node; + } - return $this->nodes; - } + return $this->nodes; + } - /** - * Checks if the given element contains - * any other block level elements - * - * @param \DOMNode $element - * @return bool - */ - public function containsBlock(DOMNode $element): bool - { - if ($element->hasChildNodes() === false) { - return false; - } + /** + * Checks if the given element contains + * any other block level elements + * + * @param \DOMNode $element + * @return bool + */ + public function containsBlock(DOMNode $element): bool + { + if ($element->hasChildNodes() === false) { + return false; + } - foreach ($element->childNodes as $childNode) { - if ($this->isBlock($childNode) === true || $this->containsBlock($childNode)) { - return true; - } - } + foreach ($element->childNodes as $childNode) { + if ($this->isBlock($childNode) === true || $this->containsBlock($childNode)) { + return true; + } + } - return false; - } + return false; + } - /** - * Takes all inline elements in the inline cache - * and combines them in a final block. The block - * will either be merged with the previous block - * if the type matches, or will be appended. - * - * The inline cache will be reset afterwards - * - * @return void - */ - public function endInlineBlock() - { - if (empty($this->inline) === true) { - return; - } + /** + * Takes all inline elements in the inline cache + * and combines them in a final block. The block + * will either be merged with the previous block + * if the type matches, or will be appended. + * + * The inline cache will be reset afterwards + * + * @return void + */ + public function endInlineBlock() + { + if (empty($this->inline) === true) { + return; + } - $html = []; + $html = []; - foreach ($this->inline as $inline) { - $node = new Inline($inline, $this->marks); - $html[] = $node->innerHTML(); - } + foreach ($this->inline as $inline) { + $node = new Inline($inline, $this->marks); + $html[] = $node->innerHTML(); + } - $innerHTML = implode(' ', $html); + $innerHTML = implode(' ', $html); - if ($fallback = $this->fallback($innerHTML)) { - $this->mergeOrAppend($fallback); - } + if ($fallback = $this->fallback($innerHTML)) { + $this->mergeOrAppend($fallback); + } - $this->inline = []; - } + $this->inline = []; + } - /** - * Creates a fallback block type for the given - * element. The element can either be a element object - * or a simple HTML/plain text string - * - * @param \Kirby\Parsley\Element|string $element - * @return array|null - */ - public function fallback($element): ?array - { - if ($fallback = $this->schema->fallback($element)) { - return $fallback; - } + /** + * Creates a fallback block type for the given + * element. The element can either be a element object + * or a simple HTML/plain text string + * + * @param \Kirby\Parsley\Element|string $element + * @return array|null + */ + public function fallback($element): ?array + { + if ($fallback = $this->schema->fallback($element)) { + return $fallback; + } - return null; - } + return null; + } - /** - * Checks if the given DOMNode is a block element - * - * @param DOMNode $element - * @return bool - */ - public function isBlock(DOMNode $element): bool - { - if (is_a($element, 'DOMElement') === false) { - return false; - } + /** + * Checks if the given DOMNode is a block element + * + * @param DOMNode $element + * @return bool + */ + public function isBlock(DOMNode $element): bool + { + if (is_a($element, 'DOMElement') === false) { + return false; + } - return array_key_exists($element->tagName, $this->nodes) === true; - } + return array_key_exists($element->tagName, $this->nodes) === true; + } - /** - * Checks if the given DOMNode is an inline element - * - * @param \DOMNode $element - * @return bool - */ - public function isInline(DOMNode $element): bool - { - if (is_a($element, 'DOMText') === true) { - return true; - } + /** + * Checks if the given DOMNode is an inline element + * + * @param \DOMNode $element + * @return bool + */ + public function isInline(DOMNode $element): bool + { + if (is_a($element, 'DOMText') === true) { + return true; + } - if (is_a($element, 'DOMElement') === true) { - // all spans will be treated as inline elements - if ($element->tagName === 'span') { - return true; - } + if (is_a($element, 'DOMElement') === true) { + // all spans will be treated as inline elements + if ($element->tagName === 'span') { + return true; + } - if ($this->containsBlock($element) === true) { - return false; - } + if ($this->containsBlock($element) === true) { + return false; + } - if ($element->tagName === 'p') { - return false; - } + if ($element->tagName === 'p') { + return false; + } - $marks = array_column($this->marks, 'tag'); - return in_array($element->tagName, $marks); - } + $marks = array_column($this->marks, 'tag'); + return in_array($element->tagName, $marks); + } - return false; - } + return false; + } - /** - * @param array $block - * @return void - */ - public function mergeOrAppend(array $block) - { - $lastIndex = count($this->blocks) - 1; - $lastItem = $this->blocks[$lastIndex] ?? null; + /** + * @param array $block + * @return void + */ + public function mergeOrAppend(array $block) + { + $lastIndex = count($this->blocks) - 1; + $lastItem = $this->blocks[$lastIndex] ?? null; - // merge with previous block - if ($block['type'] === 'text' && $lastItem && $lastItem['type'] === 'text') { - $this->blocks[$lastIndex]['content']['text'] .= ' ' . $block['content']['text']; + // merge with previous block + if ($block['type'] === 'text' && $lastItem && $lastItem['type'] === 'text') { + $this->blocks[$lastIndex]['content']['text'] .= ' ' . $block['content']['text']; - // append - } else { - $this->blocks[] = $block; - } - } + // append + } else { + $this->blocks[] = $block; + } + } - /** - * Parses the given DOM node and tries to - * convert it to a block or a list of blocks - * - * @param \DOMNode $element - * @return void - */ - public function parseNode(DOMNode $element): bool - { - $skip = ['DOMComment', 'DOMDocumentType']; + /** + * Parses the given DOM node and tries to + * convert it to a block or a list of blocks + * + * @param \DOMNode $element + * @return void + */ + public function parseNode(DOMNode $element): bool + { + $skip = ['DOMComment', 'DOMDocumentType']; - // unwanted element types - if (in_array(get_class($element), $skip) === true) { - return false; - } + // unwanted element types + if (in_array(get_class($element), $skip) === true) { + return false; + } - // inline context - if ($this->isInline($element)) { - $this->inline[] = $element; - return true; - } else { - $this->endInlineBlock(); - } + // inline context + if ($this->isInline($element)) { + $this->inline[] = $element; + return true; + } else { + $this->endInlineBlock(); + } - // known block nodes - if ($this->isBlock($element) === true) { - if ($parser = ($this->nodes[$element->tagName]['parse'] ?? null)) { - if ($result = $parser(new Element($element, $this->marks))) { - $this->blocks[] = $result; - } - } - return true; - } + // known block nodes + if ($this->isBlock($element) === true) { + if ($parser = ($this->nodes[$element->tagName]['parse'] ?? null)) { + if ($result = $parser(new Element($element, $this->marks))) { + $this->blocks[] = $result; + } + } + return true; + } - // has only unknown children (div, etc.) - if ($this->containsBlock($element) === false) { - if (in_array($element->tagName, $this->skip) === true) { - return false; - } + // has only unknown children (div, etc.) + if ($this->containsBlock($element) === false) { + if (in_array($element->tagName, $this->skip) === true) { + return false; + } - $wrappers = [ - 'body', - 'head', - 'html', - ]; + $wrappers = [ + 'body', + 'head', + 'html', + ]; - // wrapper elements should never be converted - // to a simple fallback block. Their children - // have to be parsed individually. - if (in_array($element->tagName, $wrappers) === false) { - $node = new Element($element, $this->marks); + // wrapper elements should never be converted + // to a simple fallback block. Their children + // have to be parsed individually. + if (in_array($element->tagName, $wrappers) === false) { + $node = new Element($element, $this->marks); - if ($block = $this->fallback($node)) { - $this->mergeOrAppend($block); - } + if ($block = $this->fallback($node)) { + $this->mergeOrAppend($block); + } - return true; - } - } + return true; + } + } - // parse all children - foreach ($element->childNodes as $childNode) { - $this->parseNode($childNode); - } + // parse all children + foreach ($element->childNodes as $childNode) { + $this->parseNode($childNode); + } - return true; - } + return true; + } - /** - * @return bool - */ - public function useXmlExtension(): bool - { - if (static::$useXmlExtension !== true) { - return false; - } + /** + * @return bool + */ + public function useXmlExtension(): bool + { + if (static::$useXmlExtension !== true) { + return false; + } - return Dom::isSupported(); - } + return Dom::isSupported(); + } } diff --git a/kirby/src/Parsley/Schema.php b/kirby/src/Parsley/Schema.php index 4f90b31..b9c1232 100644 --- a/kirby/src/Parsley/Schema.php +++ b/kirby/src/Parsley/Schema.php @@ -15,48 +15,48 @@ namespace Kirby\Parsley; */ class Schema { - /** - * Returns the fallback block when no - * other block type can be detected - * - * @param \Kirby\Parsley\Element|string $element - * @return array|null - */ - public function fallback($element): ?array - { - return null; - } + /** + * Returns the fallback block when no + * other block type can be detected + * + * @param \Kirby\Parsley\Element|string $element + * @return array|null + */ + public function fallback($element): ?array + { + return null; + } - /** - * Returns a list of allowed inline marks - * and their parsing rules - * - * @return array - */ - public function marks(): array - { - return []; - } + /** + * Returns a list of allowed inline marks + * and their parsing rules + * + * @return array + */ + public function marks(): array + { + return []; + } - /** - * Returns a list of allowed nodes and - * their parsing rules - * - * @return array - */ - public function nodes(): array - { - return []; - } + /** + * Returns a list of allowed nodes and + * their parsing rules + * + * @return array + */ + public function nodes(): array + { + return []; + } - /** - * Returns a list of all elements that should be - * skipped and not be parsed at all - * - * @return array - */ - public function skip(): array - { - return []; - } + /** + * Returns a list of all elements that should be + * skipped and not be parsed at all + * + * @return array + */ + public function skip(): array + { + return []; + } } diff --git a/kirby/src/Parsley/Schema/Blocks.php b/kirby/src/Parsley/Schema/Blocks.php index 676d43c..d825a0e 100644 --- a/kirby/src/Parsley/Schema/Blocks.php +++ b/kirby/src/Parsley/Schema/Blocks.php @@ -19,419 +19,419 @@ use Kirby\Toolkit\Str; */ class Blocks extends Plain { - /** - * @param \Kirby\Parsley\Element $node - * @return array - */ - public function blockquote(Element $node): array - { - $citation = null; - $text = []; + /** + * @param \Kirby\Parsley\Element $node + * @return array + */ + public function blockquote(Element $node): array + { + $citation = null; + $text = []; - // get all the text for the quote - foreach ($node->children() as $child) { - if (is_a($child, 'DOMText') === true) { - $text[] = trim($child->textContent); - } - if (is_a($child, 'DOMElement') === true && $child->tagName !== 'footer') { - $text[] = (new Element($child))->innerHTML($this->marks()); - } - } + // get all the text for the quote + foreach ($node->children() as $child) { + if (is_a($child, 'DOMText') === true) { + $text[] = trim($child->textContent); + } + if (is_a($child, 'DOMElement') === true && $child->tagName !== 'footer') { + $text[] = (new Element($child))->innerHTML($this->marks()); + } + } - // filter empty blocks and separate text blocks with breaks - $text = implode('', array_filter($text)); + // filter empty blocks and separate text blocks with breaks + $text = implode('', array_filter($text)); - // get the citation from the footer - if ($footer = $node->find('footer')) { - $citation = $footer->innerHTML($this->marks()); - } + // get the citation from the footer + if ($footer = $node->find('footer')) { + $citation = $footer->innerHTML($this->marks()); + } - return [ - 'content' => [ - 'citation' => $citation, - 'text' => $text - ], - 'type' => 'quote', - ]; - } + return [ + 'content' => [ + 'citation' => $citation, + 'text' => $text + ], + 'type' => 'quote', + ]; + } - /** - * Creates the fallback block type - * if no other block can be found - * - * @param \Kirby\Parsley\Element|string $element - * @return array|null - */ - public function fallback($element): ?array - { - if (is_a($element, Element::class) === true) { - $html = $element->innerHtml(); + /** + * Creates the fallback block type + * if no other block can be found + * + * @param \Kirby\Parsley\Element|string $element + * @return array|null + */ + public function fallback($element): ?array + { + if (is_a($element, Element::class) === true) { + $html = $element->innerHtml(); - // wrap the inner HTML in a p tag if it doesn't - // contain one yet. - if (Str::contains($html, '

    ') === false) { - $html = '

    ' . $html . '

    '; - } - } elseif (is_string($element) === true) { - $html = trim($element); + // wrap the inner HTML in a p tag if it doesn't + // contain one yet. + if (Str::contains($html, '

    ') === false) { + $html = '

    ' . $html . '

    '; + } + } elseif (is_string($element) === true) { + $html = trim($element); - if (Str::length($html) === 0) { - return null; - } + if (Str::length($html) === 0) { + return null; + } - $html = '

    ' . $html . '

    '; - } else { - return null; - } + $html = '

    ' . $html . '

    '; + } else { + return null; + } - return [ - 'content' => [ - 'text' => $html, - ], - 'type' => 'text', - ]; - } + return [ + 'content' => [ + 'text' => $html, + ], + 'type' => 'text', + ]; + } - /** - * Converts a heading element to a heading block - * - * @param \Kirby\Parsley\Element $node - * @return array - */ - public function heading(Element $node): array - { - $content = [ - 'level' => strtolower($node->tagName()), - 'text' => $node->innerHTML() - ]; + /** + * Converts a heading element to a heading block + * + * @param \Kirby\Parsley\Element $node + * @return array + */ + public function heading(Element $node): array + { + $content = [ + 'level' => strtolower($node->tagName()), + 'text' => $node->innerHTML() + ]; - if ($id = $node->attr('id')) { - $content['id'] = $id; - } + if ($id = $node->attr('id')) { + $content['id'] = $id; + } - ksort($content); + ksort($content); - return [ - 'content' => $content, - 'type' => 'heading', - ]; - } + return [ + 'content' => $content, + 'type' => 'heading', + ]; + } - /** - * @param \Kirby\Parsley\Element $node - * @return array - */ - public function iframe(Element $node): array - { - $caption = null; - $src = $node->attr('src'); + /** + * @param \Kirby\Parsley\Element $node + * @return array + */ + public function iframe(Element $node): array + { + $caption = null; + $src = $node->attr('src'); - if ($figcaption = $node->find('ancestor::figure[1]//figcaption')) { - $caption = $figcaption->innerHTML($this->marks()); + if ($figcaption = $node->find('ancestor::figure[1]//figcaption')) { + $caption = $figcaption->innerHTML($this->marks()); - // avoid parsing the caption twice - $figcaption->remove(); - } + // avoid parsing the caption twice + $figcaption->remove(); + } - // reverse engineer video URLs - if (preg_match('!player.vimeo.com\/video\/([0-9]+)!i', $src, $array) === 1) { - $src = 'https://vimeo.com/' . $array[1]; - } elseif (preg_match('!youtube.com\/embed\/([a-zA-Z0-9_-]+)!', $src, $array) === 1) { - $src = 'https://youtube.com/watch?v=' . $array[1]; - } elseif (preg_match('!youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)!', $src, $array) === 1) { - $src = 'https://youtube.com/watch?v=' . $array[1]; - } else { - $src = false; - } + // reverse engineer video URLs + if (preg_match('!player.vimeo.com\/video\/([0-9]+)!i', $src, $array) === 1) { + $src = 'https://vimeo.com/' . $array[1]; + } elseif (preg_match('!youtube.com\/embed\/([a-zA-Z0-9_-]+)!', $src, $array) === 1) { + $src = 'https://youtube.com/watch?v=' . $array[1]; + } elseif (preg_match('!youtube-nocookie.com\/embed\/([a-zA-Z0-9_-]+)!', $src, $array) === 1) { + $src = 'https://youtube.com/watch?v=' . $array[1]; + } else { + $src = false; + } - // correct video URL - if ($src) { - return [ - 'content' => [ - 'caption' => $caption, - 'url' => $src - ], - 'type' => 'video', - ]; - } + // correct video URL + if ($src) { + return [ + 'content' => [ + 'caption' => $caption, + 'url' => $src + ], + 'type' => 'video', + ]; + } - return [ - 'content' => [ - 'text' => $node->outerHTML() - ], - 'type' => 'markdown', - ]; - } + return [ + 'content' => [ + 'text' => $node->outerHTML() + ], + 'type' => 'markdown', + ]; + } - /** - * @param \Kirby\Parsley\Element $node - * @return array - */ - public function img(Element $node): array - { - $caption = null; - $link = null; + /** + * @param \Kirby\Parsley\Element $node + * @return array + */ + public function img(Element $node): array + { + $caption = null; + $link = null; - if ($figcaption = $node->find('ancestor::figure[1]//figcaption')) { - $caption = $figcaption->innerHTML($this->marks()); + if ($figcaption = $node->find('ancestor::figure[1]//figcaption')) { + $caption = $figcaption->innerHTML($this->marks()); - // avoid parsing the caption twice - $figcaption->remove(); - } + // avoid parsing the caption twice + $figcaption->remove(); + } - if ($a = $node->find('ancestor::a')) { - $link = $a->attr('href'); - } + if ($a = $node->find('ancestor::a')) { + $link = $a->attr('href'); + } - return [ - 'content' => [ - 'alt' => $node->attr('alt'), - 'caption' => $caption, - 'link' => $link, - 'location' => 'web', - 'src' => $node->attr('src'), - ], - 'type' => 'image', - ]; - } + return [ + 'content' => [ + 'alt' => $node->attr('alt'), + 'caption' => $caption, + 'link' => $link, + 'location' => 'web', + 'src' => $node->attr('src'), + ], + 'type' => 'image', + ]; + } - /** - * Converts a list element to HTML - * - * @param \Kirby\Parsley\Element $node - * @return string - */ - public function list(Element $node): string - { - $html = []; + /** + * Converts a list element to HTML + * + * @param \Kirby\Parsley\Element $node + * @return string + */ + public function list(Element $node): string + { + $html = []; - foreach ($node->filter('li') as $li) { - $innerHtml = ''; + foreach ($node->filter('li') as $li) { + $innerHtml = ''; - foreach ($li->children() as $child) { - if (is_a($child, 'DOMText') === true) { - $innerHtml .= $child->textContent; - } elseif (is_a($child, 'DOMElement') === true) { - $child = new Element($child); + foreach ($li->children() as $child) { + if (is_a($child, 'DOMText') === true) { + $innerHtml .= $child->textContent; + } elseif (is_a($child, 'DOMElement') === true) { + $child = new Element($child); - if (in_array($child->tagName(), ['ul', 'ol']) === true) { - $innerHtml .= $this->list($child); - } else { - $innerHtml .= $child->innerHTML($this->marks()); - } - } - } + if (in_array($child->tagName(), ['ul', 'ol']) === true) { + $innerHtml .= $this->list($child); + } else { + $innerHtml .= $child->innerHTML($this->marks()); + } + } + } - $html[] = '
  • ' . trim($innerHtml) . '
  • '; - } + $html[] = '
  • ' . trim($innerHtml) . '
  • '; + } - return '<' . $node->tagName() . '>' . implode($html) . 'tagName() . '>'; - } + return '<' . $node->tagName() . '>' . implode($html) . 'tagName() . '>'; + } - /** - * Returns a list of allowed inline marks - * and their parsing rules - * - * @return array - */ - public function marks(): array - { - return [ - [ - 'tag' => 'a', - 'attrs' => ['href', 'rel', 'target', 'title'], - 'defaults' => [ - 'rel' => 'noopener noreferrer' - ] - ], - [ - 'tag' => 'abbr', - ], - [ - 'tag' => 'b' - ], - [ - 'tag' => 'br', - ], - [ - 'tag' => 'code' - ], - [ - 'tag' => 'del', - ], - [ - 'tag' => 'em', - ], - [ - 'tag' => 'i', - ], - [ - 'tag' => 'p', - ], - [ - 'tag' => 'strike', - ], - [ - 'tag' => 'sub', - ], - [ - 'tag' => 'sup', - ], - [ - 'tag' => 'strong', - ], - [ - 'tag' => 'u', - ], - ]; - } + /** + * Returns a list of allowed inline marks + * and their parsing rules + * + * @return array + */ + public function marks(): array + { + return [ + [ + 'tag' => 'a', + 'attrs' => ['href', 'rel', 'target', 'title'], + 'defaults' => [ + 'rel' => 'noopener noreferrer' + ] + ], + [ + 'tag' => 'abbr', + ], + [ + 'tag' => 'b' + ], + [ + 'tag' => 'br', + ], + [ + 'tag' => 'code' + ], + [ + 'tag' => 'del', + ], + [ + 'tag' => 'em', + ], + [ + 'tag' => 'i', + ], + [ + 'tag' => 'p', + ], + [ + 'tag' => 'strike', + ], + [ + 'tag' => 'sub', + ], + [ + 'tag' => 'sup', + ], + [ + 'tag' => 'strong', + ], + [ + 'tag' => 'u', + ], + ]; + } - /** - * Returns a list of allowed nodes and - * their parsing rules - * - * @codeCoverageIgnore - * @return array - */ - public function nodes(): array - { - return [ - [ - 'tag' => 'blockquote', - 'parse' => function (Element $node) { - return $this->blockquote($node); - } - ], - [ - 'tag' => 'h1', - 'parse' => function (Element $node) { - return $this->heading($node); - } - ], - [ - 'tag' => 'h2', - 'parse' => function (Element $node) { - return $this->heading($node); - } - ], - [ - 'tag' => 'h3', - 'parse' => function (Element $node) { - return $this->heading($node); - } - ], - [ - 'tag' => 'h4', - 'parse' => function (Element $node) { - return $this->heading($node); - } - ], - [ - 'tag' => 'h5', - 'parse' => function (Element $node) { - return $this->heading($node); - } - ], - [ - 'tag' => 'h6', - 'parse' => function (Element $node) { - return $this->heading($node); - } - ], - [ - 'tag' => 'hr', - 'parse' => function (Element $node) { - return [ - 'type' => 'line' - ]; - } - ], - [ - 'tag' => 'iframe', - 'parse' => function (Element $node) { - return $this->iframe($node); - } - ], - [ - 'tag' => 'img', - 'parse' => function (Element $node) { - return $this->img($node); - } - ], - [ - 'tag' => 'ol', - 'parse' => function (Element $node) { - return [ - 'content' => [ - 'text' => $this->list($node) - ], - 'type' => 'list', - ]; - } - ], - [ - 'tag' => 'pre', - 'parse' => function (Element $node) { - return $this->pre($node); - } - ], - [ - 'tag' => 'table', - 'parse' => function (Element $node) { - return $this->table($node); - } - ], - [ - 'tag' => 'ul', - 'parse' => function (Element $node) { - return [ - 'content' => [ - 'text' => $this->list($node) - ], - 'type' => 'list', - ]; - } - ], - ]; - } + /** + * Returns a list of allowed nodes and + * their parsing rules + * + * @codeCoverageIgnore + * @return array + */ + public function nodes(): array + { + return [ + [ + 'tag' => 'blockquote', + 'parse' => function (Element $node) { + return $this->blockquote($node); + } + ], + [ + 'tag' => 'h1', + 'parse' => function (Element $node) { + return $this->heading($node); + } + ], + [ + 'tag' => 'h2', + 'parse' => function (Element $node) { + return $this->heading($node); + } + ], + [ + 'tag' => 'h3', + 'parse' => function (Element $node) { + return $this->heading($node); + } + ], + [ + 'tag' => 'h4', + 'parse' => function (Element $node) { + return $this->heading($node); + } + ], + [ + 'tag' => 'h5', + 'parse' => function (Element $node) { + return $this->heading($node); + } + ], + [ + 'tag' => 'h6', + 'parse' => function (Element $node) { + return $this->heading($node); + } + ], + [ + 'tag' => 'hr', + 'parse' => function (Element $node) { + return [ + 'type' => 'line' + ]; + } + ], + [ + 'tag' => 'iframe', + 'parse' => function (Element $node) { + return $this->iframe($node); + } + ], + [ + 'tag' => 'img', + 'parse' => function (Element $node) { + return $this->img($node); + } + ], + [ + 'tag' => 'ol', + 'parse' => function (Element $node) { + return [ + 'content' => [ + 'text' => $this->list($node) + ], + 'type' => 'list', + ]; + } + ], + [ + 'tag' => 'pre', + 'parse' => function (Element $node) { + return $this->pre($node); + } + ], + [ + 'tag' => 'table', + 'parse' => function (Element $node) { + return $this->table($node); + } + ], + [ + 'tag' => 'ul', + 'parse' => function (Element $node) { + return [ + 'content' => [ + 'text' => $this->list($node) + ], + 'type' => 'list', + ]; + } + ], + ]; + } - /** - * @param \Kirby\Parsley\Element $node - * @return array - */ - public function pre(Element $node): array - { - $language = 'text'; + /** + * @param \Kirby\Parsley\Element $node + * @return array + */ + public function pre(Element $node): array + { + $language = 'text'; - if ($code = $node->find('//code')) { - foreach ($code->classList() as $className) { - if (preg_match('!language-(.*?)!', $className)) { - $language = str_replace('language-', '', $className); - break; - } - } - } + if ($code = $node->find('//code')) { + foreach ($code->classList() as $className) { + if (preg_match('!language-(.*?)!', $className)) { + $language = str_replace('language-', '', $className); + break; + } + } + } - return [ - 'content' => [ - 'code' => $node->innerText(), - 'language' => $language - ], - 'type' => 'code', - ]; - } + return [ + 'content' => [ + 'code' => $node->innerText(), + 'language' => $language + ], + 'type' => 'code', + ]; + } - /** - * @param \Kirby\Parsley\Element $node - * @return array - */ - public function table(Element $node): array - { - return [ - 'content' => [ - 'text' => $node->outerHTML(), - ], - 'type' => 'markdown', - ]; - } + /** + * @param \Kirby\Parsley\Element $node + * @return array + */ + public function table(Element $node): array + { + return [ + 'content' => [ + 'text' => $node->outerHTML(), + ], + 'type' => 'markdown', + ]; + } } diff --git a/kirby/src/Parsley/Schema/Plain.php b/kirby/src/Parsley/Schema/Plain.php index 32ad856..96e976a 100644 --- a/kirby/src/Parsley/Schema/Plain.php +++ b/kirby/src/Parsley/Schema/Plain.php @@ -20,50 +20,50 @@ use Kirby\Toolkit\Str; */ class Plain extends Schema { - /** - * Creates the fallback block type - * if no other block can be found - * - * @param \Kirby\Parsley\Element|string $element - * @return array|null - */ - public function fallback($element): ?array - { - if (is_a($element, Element::class) === true) { - $text = $element->innerText(); - } elseif (is_string($element) === true) { - $text = trim($element); + /** + * Creates the fallback block type + * if no other block can be found + * + * @param \Kirby\Parsley\Element|string $element + * @return array|null + */ + public function fallback($element): ?array + { + if (is_a($element, Element::class) === true) { + $text = $element->innerText(); + } elseif (is_string($element) === true) { + $text = trim($element); - if (Str::length($text) === 0) { - return null; - } - } else { - return null; - } + if (Str::length($text) === 0) { + return null; + } + } else { + return null; + } - return [ - 'content' => [ - 'text' => $text - ], - 'type' => 'text', - ]; - } + return [ + 'content' => [ + 'text' => $text + ], + 'type' => 'text', + ]; + } - /** - * Returns a list of all elements that - * should be skipped during parsing - * - * @return array - */ - public function skip(): array - { - return [ - 'base', - 'link', - 'meta', - 'script', - 'style', - 'title' - ]; - } + /** + * Returns a list of all elements that + * should be skipped during parsing + * + * @return array + */ + public function skip(): array + { + return [ + 'base', + 'link', + 'meta', + 'script', + 'style', + 'title' + ]; + } } diff --git a/kirby/src/Sane/DomHandler.php b/kirby/src/Sane/DomHandler.php index e615f78..810a1ce 100644 --- a/kirby/src/Sane/DomHandler.php +++ b/kirby/src/Sane/DomHandler.php @@ -19,147 +19,147 @@ use Kirby\Toolkit\Dom; */ class DomHandler extends Handler { - /** - * List of all MIME types that may - * be used in data URIs - * - * @var array - */ - public static $allowedDataUris = [ - 'data:image/png', - 'data:image/gif', - 'data:image/jpg', - 'data:image/jpe', - 'data:image/pjp', - 'data:img/png', - 'data:img/gif', - 'data:img/jpg', - 'data:img/jpe', - 'data:img/pjp', - ]; + /** + * List of all MIME types that may + * be used in data URIs + * + * @var array + */ + public static $allowedDataUris = [ + 'data:image/png', + 'data:image/gif', + 'data:image/jpg', + 'data:image/jpe', + 'data:image/pjp', + 'data:img/png', + 'data:img/gif', + 'data:img/jpg', + 'data:img/jpe', + 'data:img/pjp', + ]; - /** - * Allowed hostnames for HTTP(S) URLs - * - * @var array - */ - public static $allowedDomains = []; + /** + * Allowed hostnames for HTTP(S) URLs + * + * @var array + */ + public static $allowedDomains = []; - /** - * Names of allowed XML processing instructions - * - * @var array - */ - public static $allowedPIs = []; + /** + * Names of allowed XML processing instructions + * + * @var array + */ + public static $allowedPIs = []; - /** - * The document type (`'HTML'` or `'XML'`) - * (to be set in child classes) - * - * @var string - */ - protected static $type = 'XML'; + /** + * The document type (`'HTML'` or `'XML'`) + * (to be set in child classes) + * + * @var string + */ + protected static $type = 'XML'; - /** - * Sanitizes the given string - * - * @param string $string - * @return string - * - * @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed - */ - public static function sanitize(string $string): string - { - $dom = static::parse($string); - $dom->sanitize(static::options()); - return $dom->toString(); - } + /** + * Sanitizes the given string + * + * @param string $string + * @return string + * + * @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed + */ + public static function sanitize(string $string): string + { + $dom = static::parse($string); + $dom->sanitize(static::options()); + return $dom->toString(); + } - /** - * Validates file contents - * - * @param string $string - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - */ - public static function validate(string $string): void - { - $dom = static::parse($string); - $errors = $dom->sanitize(static::options()); - if (count($errors) > 0) { - // there may be multiple errors, we can only throw one of them at a time - throw $errors[0]; - } - } + /** + * Validates file contents + * + * @param string $string + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + */ + public static function validate(string $string): void + { + $dom = static::parse($string); + $errors = $dom->sanitize(static::options()); + if (count($errors) > 0) { + // there may be multiple errors, we can only throw one of them at a time + throw $errors[0]; + } + } - /** - * Custom callback for additional attribute sanitization - * @internal - * - * @param \DOMAttr $attr - * @return array Array with exception objects for each modification - */ - public static function sanitizeAttr(DOMAttr $attr): array - { - // to be extended in child classes - return []; - } + /** + * Custom callback for additional attribute sanitization + * @internal + * + * @param \DOMAttr $attr + * @return array Array with exception objects for each modification + */ + public static function sanitizeAttr(DOMAttr $attr): array + { + // to be extended in child classes + return []; + } - /** - * Custom callback for additional element sanitization - * @internal - * - * @param \DOMElement $element - * @return array Array with exception objects for each modification - */ - public static function sanitizeElement(DOMElement $element): array - { - // to be extended in child classes - return []; - } + /** + * Custom callback for additional element sanitization + * @internal + * + * @param \DOMElement $element + * @return array Array with exception objects for each modification + */ + public static function sanitizeElement(DOMElement $element): array + { + // to be extended in child classes + return []; + } - /** - * Custom callback for additional doctype validation - * @internal - * - * @param \DOMDocumentType $doctype - * @return void - */ - public static function validateDoctype(DOMDocumentType $doctype): void - { - // to be extended in child classes - } + /** + * Custom callback for additional doctype validation + * @internal + * + * @param \DOMDocumentType $doctype + * @return void + */ + public static function validateDoctype(DOMDocumentType $doctype): void + { + // to be extended in child classes + } - /** - * Returns the sanitization options for the handler - * (to be extended in child classes) - * - * @return array - */ - protected static function options(): array - { - return [ - 'allowedDataUris' => static::$allowedDataUris, - 'allowedDomains' => static::$allowedDomains, - 'allowedPIs' => static::$allowedPIs, - 'attrCallback' => [static::class, 'sanitizeAttr'], - 'doctypeCallback' => [static::class, 'validateDoctype'], - 'elementCallback' => [static::class, 'sanitizeElement'], - ]; - } + /** + * Returns the sanitization options for the handler + * (to be extended in child classes) + * + * @return array + */ + protected static function options(): array + { + return [ + 'allowedDataUris' => static::$allowedDataUris, + 'allowedDomains' => static::$allowedDomains, + 'allowedPIs' => static::$allowedPIs, + 'attrCallback' => [static::class, 'sanitizeAttr'], + 'doctypeCallback' => [static::class, 'validateDoctype'], + 'elementCallback' => [static::class, 'sanitizeElement'], + ]; + } - /** - * Parses the given string into a `Toolkit\Dom` object - * - * @param string $string - * @return \Kirby\Toolkit\Dom - * - * @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed - */ - protected static function parse(string $string) - { - return new Dom($string, static::$type); - } + /** + * Parses the given string into a `Toolkit\Dom` object + * + * @param string $string + * @return \Kirby\Toolkit\Dom + * + * @throws \Kirby\Exception\InvalidArgumentException If the file couldn't be parsed + */ + protected static function parse(string $string) + { + return new Dom($string, static::$type); + } } diff --git a/kirby/src/Sane/Handler.php b/kirby/src/Sane/Handler.php index 5fc6e4f..c66bc98 100644 --- a/kirby/src/Sane/Handler.php +++ b/kirby/src/Sane/Handler.php @@ -19,73 +19,73 @@ use Kirby\Filesystem\F; */ abstract class Handler { - /** - * Sanitizes the given string - * - * @param string $string - * @return string - */ - abstract public static function sanitize(string $string): string; + /** + * Sanitizes the given string + * + * @param string $string + * @return string + */ + abstract public static function sanitize(string $string): string; - /** - * Sanitizes the contents of a file by overwriting - * the file with the sanitized version - * - * @param string $file - * @return void - * - * @throws \Kirby\Exception\Exception If the file does not exist - * @throws \Kirby\Exception\Exception On other errors - */ - public static function sanitizeFile(string $file): void - { - $sanitized = static::sanitize(static::readFile($file)); - F::write($file, $sanitized); - } + /** + * Sanitizes the contents of a file by overwriting + * the file with the sanitized version + * + * @param string $file + * @return void + * + * @throws \Kirby\Exception\Exception If the file does not exist + * @throws \Kirby\Exception\Exception On other errors + */ + public static function sanitizeFile(string $file): void + { + $sanitized = static::sanitize(static::readFile($file)); + F::write($file, $sanitized); + } - /** - * Validates file contents - * - * @param string $string - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\Exception On other errors - */ - abstract public static function validate(string $string): void; + /** + * Validates file contents + * + * @param string $string + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\Exception On other errors + */ + abstract public static function validate(string $string): void; - /** - * Validates the contents of a file - * - * @param string $file - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\Exception If the file does not exist - * @throws \Kirby\Exception\Exception On other errors - */ - public static function validateFile(string $file): void - { - static::validate(static::readFile($file)); - } + /** + * Validates the contents of a file + * + * @param string $file + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\Exception If the file does not exist + * @throws \Kirby\Exception\Exception On other errors + */ + public static function validateFile(string $file): void + { + static::validate(static::readFile($file)); + } - /** - * Reads the contents of a file - * for sanitization or validation - * - * @param string $file - * @return string - * - * @throws \Kirby\Exception\Exception If the file does not exist - */ - protected static function readFile(string $file): string - { - $contents = F::read($file); + /** + * Reads the contents of a file + * for sanitization or validation + * + * @param string $file + * @return string + * + * @throws \Kirby\Exception\Exception If the file does not exist + */ + protected static function readFile(string $file): string + { + $contents = F::read($file); - if ($contents === false) { - throw new Exception('The file "' . $file . '" does not exist'); - } + if ($contents === false) { + throw new Exception('The file "' . $file . '" does not exist'); + } - return $contents; - } + return $contents; + } } diff --git a/kirby/src/Sane/Html.php b/kirby/src/Sane/Html.php index 56ff50b..99823e5 100644 --- a/kirby/src/Sane/Html.php +++ b/kirby/src/Sane/Html.php @@ -15,130 +15,130 @@ namespace Kirby\Sane; */ class Html extends DomHandler { - /** - * Global list of allowed attribute prefixes - * - * @var array - */ - public static $allowedAttrPrefixes = [ - 'aria-', - 'data-', - ]; + /** + * Global list of allowed attribute prefixes + * + * @var array + */ + public static $allowedAttrPrefixes = [ + 'aria-', + 'data-', + ]; - /** - * Global list of allowed attributes - * - * @var array - */ - public static $allowedAttrs = [ - 'class', - 'id', - ]; + /** + * Global list of allowed attributes + * + * @var array + */ + public static $allowedAttrs = [ + 'class', + 'id', + ]; - /** - * Allowed hostnames for HTTP(S) URLs - * - * @var array - */ - public static $allowedDomains = true; + /** + * Allowed hostnames for HTTP(S) URLs + * + * @var array + */ + public static $allowedDomains = true; - /** - * Associative array of all allowed tag names with the value - * of either an array with the list of all allowed attributes - * for this tag, `true` to allow any attribute from the - * `allowedAttrs` list or `false` to allow the tag without - * any attributes - * - * @var array - */ - public static $allowedTags = [ - 'a' => ['href', 'rel', 'title', 'target'], - 'abbr' => ['title'], - 'b' => true, - 'body' => true, - 'blockquote' => true, - 'br' => true, - 'code' => true, - 'dl' => true, - 'dd' => true, - 'del' => true, - 'div' => true, - 'dt' => true, - 'em' => true, - 'footer' => true, - 'h1' => true, - 'h2' => true, - 'h3' => true, - 'h4' => true, - 'h5' => true, - 'h6' => true, - 'hr' => true, - 'html' => true, - 'i' => true, - 'ins' => true, - 'li' => true, - 'small' => true, - 'span' => true, - 'strong' => true, - 'sub' => true, - 'sup' => true, - 'ol' => true, - 'p' => true, - 'pre' => true, - 's' => true, - 'u' => true, - 'ul' => true, - ]; + /** + * Associative array of all allowed tag names with the value + * of either an array with the list of all allowed attributes + * for this tag, `true` to allow any attribute from the + * `allowedAttrs` list or `false` to allow the tag without + * any attributes + * + * @var array + */ + public static $allowedTags = [ + 'a' => ['href', 'rel', 'title', 'target'], + 'abbr' => ['title'], + 'b' => true, + 'body' => true, + 'blockquote' => true, + 'br' => true, + 'code' => true, + 'dl' => true, + 'dd' => true, + 'del' => true, + 'div' => true, + 'dt' => true, + 'em' => true, + 'footer' => true, + 'h1' => true, + 'h2' => true, + 'h3' => true, + 'h4' => true, + 'h5' => true, + 'h6' => true, + 'hr' => true, + 'html' => true, + 'i' => true, + 'ins' => true, + 'li' => true, + 'small' => true, + 'span' => true, + 'strong' => true, + 'sub' => true, + 'sup' => true, + 'ol' => true, + 'p' => true, + 'pre' => true, + 's' => true, + 'u' => true, + 'ul' => true, + ]; - /** - * Array of explicitly disallowed tags - * - * IMPORTANT: Use lower-case names here because - * of the case-insensitive matching - * - * @var array - */ - public static $disallowedTags = [ - 'iframe', - 'meta', - 'object', - 'script', - 'style', - ]; + /** + * Array of explicitly disallowed tags + * + * IMPORTANT: Use lower-case names here because + * of the case-insensitive matching + * + * @var array + */ + public static $disallowedTags = [ + 'iframe', + 'meta', + 'object', + 'script', + 'style', + ]; - /** - * List of attributes that may contain URLs - * - * @var array - */ - public static $urlAttrs = [ - 'href', - 'src', - 'xlink:href', - ]; + /** + * List of attributes that may contain URLs + * + * @var array + */ + public static $urlAttrs = [ + 'href', + 'src', + 'xlink:href', + ]; - /** - * The document type (`'HTML'` or `'XML'`) - * - * @var string - */ - protected static $type = 'HTML'; + /** + * The document type (`'HTML'` or `'XML'`) + * + * @var string + */ + protected static $type = 'HTML'; - /** - * Returns the sanitization options for the handler - * - * @return array - */ - protected static function options(): array - { - return array_merge(parent::options(), [ - 'allowedAttrPrefixes' => static::$allowedAttrPrefixes, - 'allowedAttrs' => static::$allowedAttrs, - 'allowedNamespaces' => [], - 'allowedPIs' => [], - 'allowedTags' => static::$allowedTags, - 'disallowedTags' => static::$disallowedTags, - 'urlAttrs' => static::$urlAttrs, - ]); - } + /** + * Returns the sanitization options for the handler + * + * @return array + */ + protected static function options(): array + { + return array_merge(parent::options(), [ + 'allowedAttrPrefixes' => static::$allowedAttrPrefixes, + 'allowedAttrs' => static::$allowedAttrs, + 'allowedNamespaces' => [], + 'allowedPIs' => [], + 'allowedTags' => static::$allowedTags, + 'disallowedTags' => static::$disallowedTags, + 'urlAttrs' => static::$urlAttrs, + ]); + } } diff --git a/kirby/src/Sane/Sane.php b/kirby/src/Sane/Sane.php index 140a2d9..5a55ae0 100644 --- a/kirby/src/Sane/Sane.php +++ b/kirby/src/Sane/Sane.php @@ -21,189 +21,189 @@ use Kirby\Filesystem\F; */ class Sane { - /** - * Handler Type Aliases - * - * @var array - */ - public static $aliases = [ - 'application/xml' => 'xml', - 'image/svg' => 'svg', - 'image/svg+xml' => 'svg', - 'text/html' => 'html', - 'text/xml' => 'xml', - ]; + /** + * Handler Type Aliases + * + * @var array + */ + public static $aliases = [ + 'application/xml' => 'xml', + 'image/svg' => 'svg', + 'image/svg+xml' => 'svg', + 'text/html' => 'html', + 'text/xml' => 'xml', + ]; - /** - * All registered handlers - * - * @var array - */ - public static $handlers = [ - 'html' => 'Kirby\Sane\Html', - 'svg' => 'Kirby\Sane\Svg', - 'svgz' => 'Kirby\Sane\Svgz', - 'xml' => 'Kirby\Sane\Xml', - ]; + /** + * All registered handlers + * + * @var array + */ + public static $handlers = [ + 'html' => 'Kirby\Sane\Html', + 'svg' => 'Kirby\Sane\Svg', + 'svgz' => 'Kirby\Sane\Svgz', + 'xml' => 'Kirby\Sane\Xml', + ]; - /** - * Handler getter - * - * @param string $type - * @param bool $lazy If set to `true`, `null` is returned for undefined handlers - * @return \Kirby\Sane\Handler|null - * - * @throws \Kirby\Exception\NotFoundException If no handler was found and `$lazy` was set to `false` - */ - public static function handler(string $type, bool $lazy = false) - { - // normalize the type - $type = mb_strtolower($type); + /** + * Handler getter + * + * @param string $type + * @param bool $lazy If set to `true`, `null` is returned for undefined handlers + * @return \Kirby\Sane\Handler|null + * + * @throws \Kirby\Exception\NotFoundException If no handler was found and `$lazy` was set to `false` + */ + public static function handler(string $type, bool $lazy = false) + { + // normalize the type + $type = mb_strtolower($type); - // find a handler or alias - $handler = static::$handlers[$type] ?? - static::$handlers[static::$aliases[$type] ?? null] ?? - null; + // find a handler or alias + $handler = static::$handlers[$type] ?? + static::$handlers[static::$aliases[$type] ?? null] ?? + null; - if (empty($handler) === false && class_exists($handler) === true) { - return new $handler(); - } + if (empty($handler) === false && class_exists($handler) === true) { + return new $handler(); + } - if ($lazy === true) { - return null; - } + if ($lazy === true) { + return null; + } - throw new NotFoundException('Missing handler for type: "' . $type . '"'); - } + throw new NotFoundException('Missing handler for type: "' . $type . '"'); + } - /** - * Sanitizes the given string with the specified handler - * @since 3.6.0 - * - * @param string $string - * @param string $type - * @return string - */ - public static function sanitize(string $string, string $type): string - { - return static::handler($type)->sanitize($string); - } + /** + * Sanitizes the given string with the specified handler + * @since 3.6.0 + * + * @param string $string + * @param string $type + * @return string + */ + public static function sanitize(string $string, string $type): string + { + return static::handler($type)->sanitize($string); + } - /** - * Sanitizes the contents of a file by overwriting - * the file with the sanitized version; - * the sane handlers are automatically chosen by - * the extension and MIME type if not specified - * @since 3.6.0 - * - * @param string $file - * @param string|bool $typeLazy Explicit handler type string, - * `true` for lazy autodetection or - * `false` for normal autodetection - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\LogicException If more than one handler applies - * @throws \Kirby\Exception\NotFoundException If the handler was not found - * @throws \Kirby\Exception\Exception On other errors - */ - public static function sanitizeFile(string $file, $typeLazy = false): void - { - if (is_string($typeLazy) === true) { - static::handler($typeLazy)->sanitizeFile($file); - return; - } + /** + * Sanitizes the contents of a file by overwriting + * the file with the sanitized version; + * the sane handlers are automatically chosen by + * the extension and MIME type if not specified + * @since 3.6.0 + * + * @param string $file + * @param string|bool $typeLazy Explicit handler type string, + * `true` for lazy autodetection or + * `false` for normal autodetection + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\LogicException If more than one handler applies + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public static function sanitizeFile(string $file, $typeLazy = false): void + { + if (is_string($typeLazy) === true) { + static::handler($typeLazy)->sanitizeFile($file); + return; + } - // try to find exactly one matching handler - $handlers = static::handlersForFile($file, $typeLazy === true); - switch (count($handlers)) { - case 0: - // lazy autodetection didn't find a handler - break; - case 1: - $handlers[0]->sanitizeFile($file); - break; - default: - // more than one matching handler; - // sanitizing with all handlers will not leave much in the output - $handlerNames = array_map('get_class', $handlers); - throw new LogicException( - 'Cannot sanitize file as more than one handler applies: ' . - implode(', ', $handlerNames) - ); - } - } + // try to find exactly one matching handler + $handlers = static::handlersForFile($file, $typeLazy === true); + switch (count($handlers)) { + case 0: + // lazy autodetection didn't find a handler + break; + case 1: + $handlers[0]->sanitizeFile($file); + break; + default: + // more than one matching handler; + // sanitizing with all handlers will not leave much in the output + $handlerNames = array_map('get_class', $handlers); + throw new LogicException( + 'Cannot sanitize file as more than one handler applies: ' . + implode(', ', $handlerNames) + ); + } + } - /** - * Validates file contents with the specified handler - * - * @param string $string - * @param string $type - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\NotFoundException If the handler was not found - * @throws \Kirby\Exception\Exception On other errors - */ - public static function validate(string $string, string $type): void - { - static::handler($type)->validate($string); - } + /** + * Validates file contents with the specified handler + * + * @param string $string + * @param string $type + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public static function validate(string $string, string $type): void + { + static::handler($type)->validate($string); + } - /** - * Validates the contents of a file; - * the sane handlers are automatically chosen by - * the extension and MIME type if not specified - * - * @param string $file - * @param string|bool $typeLazy Explicit handler type string, - * `true` for lazy autodetection or - * `false` for normal autodetection - * @return void - * - * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation - * @throws \Kirby\Exception\NotFoundException If the handler was not found - * @throws \Kirby\Exception\Exception On other errors - */ - public static function validateFile(string $file, $typeLazy = false): void - { - if (is_string($typeLazy) === true) { - static::handler($typeLazy)->validateFile($file); - return; - } + /** + * Validates the contents of a file; + * the sane handlers are automatically chosen by + * the extension and MIME type if not specified + * + * @param string $file + * @param string|bool $typeLazy Explicit handler type string, + * `true` for lazy autodetection or + * `false` for normal autodetection + * @return void + * + * @throws \Kirby\Exception\InvalidArgumentException If the file didn't pass validation + * @throws \Kirby\Exception\NotFoundException If the handler was not found + * @throws \Kirby\Exception\Exception On other errors + */ + public static function validateFile(string $file, $typeLazy = false): void + { + if (is_string($typeLazy) === true) { + static::handler($typeLazy)->validateFile($file); + return; + } - foreach (static::handlersForFile($file, $typeLazy === true) as $handler) { - $handler->validateFile($file); - } - } + foreach (static::handlersForFile($file, $typeLazy === true) as $handler) { + $handler->validateFile($file); + } + } - /** - * Returns all handler objects that apply to the given file based on - * file extension and MIME type - * - * @param string $file - * @param bool $lazy If set to `true`, undefined handlers are skipped - * @return array<\Kirby\Sane\Handler> - */ - protected static function handlersForFile(string $file, bool $lazy = false): array - { - $handlers = $handlerClasses = []; + /** + * Returns all handler objects that apply to the given file based on + * file extension and MIME type + * + * @param string $file + * @param bool $lazy If set to `true`, undefined handlers are skipped + * @return array<\Kirby\Sane\Handler> + */ + protected static function handlersForFile(string $file, bool $lazy = false): array + { + $handlers = $handlerClasses = []; - // all values that can be used for the handler search; - // filter out all empty options - $options = array_filter([F::extension($file), F::mime($file)]); + // all values that can be used for the handler search; + // filter out all empty options + $options = array_filter([F::extension($file), F::mime($file)]); - foreach ($options as $option) { - $handler = static::handler($option, $lazy); - $handlerClass = $handler ? get_class($handler) : null; + foreach ($options as $option) { + $handler = static::handler($option, $lazy); + $handlerClass = $handler ? get_class($handler) : null; - // ensure that each handler class is only returned once - if ($handler && in_array($handlerClass, $handlerClasses) === false) { - $handlers[] = $handler; - $handlerClasses[] = $handlerClass; - } - } + // ensure that each handler class is only returned once + if ($handler && in_array($handlerClass, $handlerClasses) === false) { + $handlers[] = $handler; + $handlerClasses[] = $handlerClass; + } + } - return $handlers; - } + return $handlers; + } } diff --git a/kirby/src/Sane/Svg.php b/kirby/src/Sane/Svg.php index c0772b3..d8d8604 100644 --- a/kirby/src/Sane/Svg.php +++ b/kirby/src/Sane/Svg.php @@ -23,487 +23,487 @@ use Kirby\Toolkit\Str; */ class Svg extends Xml { - /** - * Allow and block lists are inspired by DOMPurify - * - * @link https://github.com/cure53/DOMPurify - * @copyright 2015 Mario Heiderich - * @license https://www.apache.org/licenses/LICENSE-2.0 - */ + /** + * Allow and block lists are inspired by DOMPurify + * + * @link https://github.com/cure53/DOMPurify + * @copyright 2015 Mario Heiderich + * @license https://www.apache.org/licenses/LICENSE-2.0 + */ - /** - * Global list of allowed attribute prefixes - * - * @var array - */ - public static $allowedAttrPrefixes = [ - 'aria-', - 'data-', - ]; + /** + * Global list of allowed attribute prefixes + * + * @var array + */ + public static $allowedAttrPrefixes = [ + 'aria-', + 'data-', + ]; - /** - * Global list of allowed attributes - * - * @var array - */ - public static $allowedAttrs = [ - // core attributes - 'id', - 'lang', - 'tabindex', - 'xml:id', - 'xml:lang', - 'xml:space', + /** + * Global list of allowed attributes + * + * @var array + */ + public static $allowedAttrs = [ + // core attributes + 'id', + 'lang', + 'tabindex', + 'xml:id', + 'xml:lang', + 'xml:space', - // styling attributes - 'class', - 'style', + // styling attributes + 'class', + 'style', - // conditional processing attributes - 'systemLanguage', + // conditional processing attributes + 'systemLanguage', - // presentation attributes - 'alignment-baseline', - 'baseline-shift', - 'clip', - 'clip-path', - 'clip-rule', - 'color', - 'color-interpolation', - 'color-interpolation-filters', - 'color-profile', - 'color-rendering', - 'd', - 'direction', - 'display', - 'dominant-baseline', - 'enable-background', - 'fill', - 'fill-opacity', - 'fill-rule', - 'filter', - 'flood-color', - 'flood-opacity', - 'font-family', - 'font-size', - 'font-size-adjust', - 'font-stretch', - 'font-style', - 'font-variant', - 'font-weight', - 'image-rendering', - 'kerning', - 'letter-spacing', - 'lighting-color', - 'marker-end', - 'marker-mid', - 'marker-start', - 'mask', - 'opacity', - 'overflow', - 'paint-order', - 'shape-rendering', - 'stop-color', - 'stop-opacity', - 'stroke', - 'stroke-dasharray', - 'stroke-dashoffset', - 'stroke-linecap', - 'stroke-linejoin', - 'stroke-miterlimit', - 'stroke-opacity', - 'stroke-width', - 'text-anchor', - 'text-decoration', - 'text-rendering', - 'transform', - 'visibility', - 'word-spacing', - 'writing-mode', + // presentation attributes + 'alignment-baseline', + 'baseline-shift', + 'clip', + 'clip-path', + 'clip-rule', + 'color', + 'color-interpolation', + 'color-interpolation-filters', + 'color-profile', + 'color-rendering', + 'd', + 'direction', + 'display', + 'dominant-baseline', + 'enable-background', + 'fill', + 'fill-opacity', + 'fill-rule', + 'filter', + 'flood-color', + 'flood-opacity', + 'font-family', + 'font-size', + 'font-size-adjust', + 'font-stretch', + 'font-style', + 'font-variant', + 'font-weight', + 'image-rendering', + 'kerning', + 'letter-spacing', + 'lighting-color', + 'marker-end', + 'marker-mid', + 'marker-start', + 'mask', + 'opacity', + 'overflow', + 'paint-order', + 'shape-rendering', + 'stop-color', + 'stop-opacity', + 'stroke', + 'stroke-dasharray', + 'stroke-dashoffset', + 'stroke-linecap', + 'stroke-linejoin', + 'stroke-miterlimit', + 'stroke-opacity', + 'stroke-width', + 'text-anchor', + 'text-decoration', + 'text-rendering', + 'transform', + 'visibility', + 'word-spacing', + 'writing-mode', - // animation attribute target attributes - 'attributeName', - 'attributeType', + // animation attribute target attributes + 'attributeName', + 'attributeType', - // animation timing attributes - 'begin', - 'dur', - 'end', - 'max', - 'min', - 'repeatCount', - 'repeatDur', - 'restart', + // animation timing attributes + 'begin', + 'dur', + 'end', + 'max', + 'min', + 'repeatCount', + 'repeatDur', + 'restart', - // animation value attributes - 'by', - 'from', - 'keySplines', - 'keyTimes', - 'to', - 'values', + // animation value attributes + 'by', + 'from', + 'keySplines', + 'keyTimes', + 'to', + 'values', - // animation addition attributes - 'accumulate', - 'additive', + // animation addition attributes + 'accumulate', + 'additive', - // filter primitive attributes - 'height', - 'result', - 'width', - 'x', - 'y', + // filter primitive attributes + 'height', + 'result', + 'width', + 'x', + 'y', - // transfer function attributes - 'amplitude', - 'exponent', - 'intercept', - 'offset', - 'slope', - 'tableValues', - 'type', + // transfer function attributes + 'amplitude', + 'exponent', + 'intercept', + 'offset', + 'slope', + 'tableValues', + 'type', - // other attributes specific to one or multiple elements - 'azimuth', - 'baseFrequency', - 'bias', - 'clipPathUnits', - 'cx', - 'cy', - 'diffuseConstant', - 'divisor', - 'dx', - 'dy', - 'edgeMode', - 'elevation', - 'filterUnits', - 'fr', - 'fx', - 'fy', - 'g1', - 'g2', - 'glyph-name', - 'glyphRef', - 'gradientTransform', - 'gradientUnits', - 'href', - 'hreflang', - 'in', - 'in2', - 'k', - 'k1', - 'k2', - 'k3', - 'k4', - 'kernelMatrix', - 'kernelUnitLength', - 'keyPoints', - 'lengthAdjust', - 'limitingConeAngle', - 'markerHeight', - 'markerUnits', - 'markerWidth', - 'maskContentUnits', - 'maskUnits', - 'media', - 'method', - 'mode', - 'numOctaves', - 'operator', - 'order', - 'orient', - 'orientation', - 'path', - 'pathLength', - 'patternContentUnits', - 'patternTransform', - 'patternUnits', - 'points', - 'pointsAtX', - 'pointsAtY', - 'pointsAtZ', - 'preserveAlpha', - 'preserveAspectRatio', - 'primitiveUnits', - 'r', - 'radius', - 'refX', - 'refY', - 'rotate', - 'rx', - 'ry', - 'scale', - 'seed', - 'side', - 'spacing', - 'specularConstant', - 'specularExponent', - 'spreadMethod', - 'startOffset', - 'stdDeviation', - 'stitchTiles', - 'surfaceScale', - 'targetX', - 'targetY', - 'textLength', - 'u1', - 'u2', - 'unicode', - 'version', - 'vert-adv-y', - 'vert-origin-x', - 'vert-origin-y', - 'viewBox', - 'x1', - 'x2', - 'xChannelSelector', - 'xlink:href', - 'xlink:title', - 'y1', - 'y2', - 'yChannelSelector', - 'z', - 'zoomAndPan', - ]; + // other attributes specific to one or multiple elements + 'azimuth', + 'baseFrequency', + 'bias', + 'clipPathUnits', + 'cx', + 'cy', + 'diffuseConstant', + 'divisor', + 'dx', + 'dy', + 'edgeMode', + 'elevation', + 'filterUnits', + 'fr', + 'fx', + 'fy', + 'g1', + 'g2', + 'glyph-name', + 'glyphRef', + 'gradientTransform', + 'gradientUnits', + 'href', + 'hreflang', + 'in', + 'in2', + 'k', + 'k1', + 'k2', + 'k3', + 'k4', + 'kernelMatrix', + 'kernelUnitLength', + 'keyPoints', + 'lengthAdjust', + 'limitingConeAngle', + 'markerHeight', + 'markerUnits', + 'markerWidth', + 'maskContentUnits', + 'maskUnits', + 'media', + 'method', + 'mode', + 'numOctaves', + 'operator', + 'order', + 'orient', + 'orientation', + 'path', + 'pathLength', + 'patternContentUnits', + 'patternTransform', + 'patternUnits', + 'points', + 'pointsAtX', + 'pointsAtY', + 'pointsAtZ', + 'preserveAlpha', + 'preserveAspectRatio', + 'primitiveUnits', + 'r', + 'radius', + 'refX', + 'refY', + 'rotate', + 'rx', + 'ry', + 'scale', + 'seed', + 'side', + 'spacing', + 'specularConstant', + 'specularExponent', + 'spreadMethod', + 'startOffset', + 'stdDeviation', + 'stitchTiles', + 'surfaceScale', + 'targetX', + 'targetY', + 'textLength', + 'u1', + 'u2', + 'unicode', + 'version', + 'vert-adv-y', + 'vert-origin-x', + 'vert-origin-y', + 'viewBox', + 'x1', + 'x2', + 'xChannelSelector', + 'xlink:href', + 'xlink:title', + 'y1', + 'y2', + 'yChannelSelector', + 'z', + 'zoomAndPan', + ]; - /** - * Associative array of all allowed namespace URIs - * - * @var array - */ - public static $allowedNamespaces = [ - '' => 'http://www.w3.org/2000/svg', - 'xlink' => 'http://www.w3.org/1999/xlink' - ]; + /** + * Associative array of all allowed namespace URIs + * + * @var array + */ + public static $allowedNamespaces = [ + '' => 'http://www.w3.org/2000/svg', + 'xlink' => 'http://www.w3.org/1999/xlink' + ]; - /** - * Associative array of all allowed tag names with the value - * of either an array with the list of all allowed attributes - * for this tag, `true` to allow any attribute from the - * `allowedAttrs` list or `false` to allow the tag without - * any attributes - * - * @var array - */ - public static $allowedTags = [ - 'a' => true, - 'altGlyph' => true, - 'altGlyphDef' => true, - 'altGlyphItem' => true, - 'animateColor' => true, - 'animateMotion' => true, - 'animateTransform' => true, - 'circle' => true, - 'clipPath' => true, - 'defs' => true, - 'desc' => true, - 'ellipse' => true, - 'feBlend' => true, - 'feColorMatrix' => true, - 'feComponentTransfer' => true, - 'feComposite' => true, - 'feConvolveMatrix' => true, - 'feDiffuseLighting' => true, - 'feDisplacementMap' => true, - 'feDistantLight' => true, - 'feFlood' => true, - 'feFuncA' => true, - 'feFuncB' => true, - 'feFuncG' => true, - 'feFuncR' => true, - 'feGaussianBlur' => true, - 'feMerge' => true, - 'feMergeNode' => true, - 'feMorphology' => true, - 'feOffset' => true, - 'fePointLight' => true, - 'feSpecularLighting' => true, - 'feSpotLight' => true, - 'feTile' => true, - 'feTurbulence' => true, - 'filter' => true, - 'font' => true, - 'g' => true, - 'glyph' => true, - 'glyphRef' => true, - 'hkern' => true, - 'image' => true, - 'line' => true, - 'linearGradient' => true, - 'marker' => true, - 'mask' => true, - 'metadata' => true, - 'mpath' => true, - 'path' => true, - 'pattern' => true, - 'polygon' => true, - 'polyline' => true, - 'radialGradient' => true, - 'rect' => true, - 'stop' => true, - 'style' => true, - 'svg' => true, - 'switch' => true, - 'symbol' => true, - 'text' => true, - 'textPath' => true, - 'title' => true, - 'tref' => true, - 'tspan' => true, - 'use' => true, - 'view' => true, - 'vkern' => true, - ]; + /** + * Associative array of all allowed tag names with the value + * of either an array with the list of all allowed attributes + * for this tag, `true` to allow any attribute from the + * `allowedAttrs` list or `false` to allow the tag without + * any attributes + * + * @var array + */ + public static $allowedTags = [ + 'a' => true, + 'altGlyph' => true, + 'altGlyphDef' => true, + 'altGlyphItem' => true, + 'animateColor' => true, + 'animateMotion' => true, + 'animateTransform' => true, + 'circle' => true, + 'clipPath' => true, + 'defs' => true, + 'desc' => true, + 'ellipse' => true, + 'feBlend' => true, + 'feColorMatrix' => true, + 'feComponentTransfer' => true, + 'feComposite' => true, + 'feConvolveMatrix' => true, + 'feDiffuseLighting' => true, + 'feDisplacementMap' => true, + 'feDistantLight' => true, + 'feFlood' => true, + 'feFuncA' => true, + 'feFuncB' => true, + 'feFuncG' => true, + 'feFuncR' => true, + 'feGaussianBlur' => true, + 'feMerge' => true, + 'feMergeNode' => true, + 'feMorphology' => true, + 'feOffset' => true, + 'fePointLight' => true, + 'feSpecularLighting' => true, + 'feSpotLight' => true, + 'feTile' => true, + 'feTurbulence' => true, + 'filter' => true, + 'font' => true, + 'g' => true, + 'glyph' => true, + 'glyphRef' => true, + 'hkern' => true, + 'image' => true, + 'line' => true, + 'linearGradient' => true, + 'marker' => true, + 'mask' => true, + 'metadata' => true, + 'mpath' => true, + 'path' => true, + 'pattern' => true, + 'polygon' => true, + 'polyline' => true, + 'radialGradient' => true, + 'rect' => true, + 'stop' => true, + 'style' => true, + 'svg' => true, + 'switch' => true, + 'symbol' => true, + 'text' => true, + 'textPath' => true, + 'title' => true, + 'tref' => true, + 'tspan' => true, + 'use' => true, + 'view' => true, + 'vkern' => true, + ]; - /** - * Array of explicitly disallowed tags - * - * IMPORTANT: Use lower-case names here because - * of the case-insensitive matching - * - * @var array - */ - public static $disallowedTags = [ - 'animate', - 'color-profile', - 'cursor', - 'discard', - 'fedropshadow', - 'feimage', - 'font-face', - 'font-face-format', - 'font-face-name', - 'font-face-src', - 'font-face-uri', - 'foreignobject', - 'hatch', - 'hatchpath', - 'mesh', - 'meshgradient', - 'meshpatch', - 'meshrow', - 'missing-glyph', - 'script', - 'set', - 'solidcolor', - 'unknown', - ]; + /** + * Array of explicitly disallowed tags + * + * IMPORTANT: Use lower-case names here because + * of the case-insensitive matching + * + * @var array + */ + public static $disallowedTags = [ + 'animate', + 'color-profile', + 'cursor', + 'discard', + 'fedropshadow', + 'feimage', + 'font-face', + 'font-face-format', + 'font-face-name', + 'font-face-src', + 'font-face-uri', + 'foreignobject', + 'hatch', + 'hatchpath', + 'mesh', + 'meshgradient', + 'meshpatch', + 'meshrow', + 'missing-glyph', + 'script', + 'set', + 'solidcolor', + 'unknown', + ]; - /** - * Custom callback for additional attribute sanitization - * @internal - * - * @param \DOMAttr $attr - * @return array Array with exception objects for each modification - */ - public static function sanitizeAttr(DOMAttr $attr): array - { - $element = $attr->ownerElement; - $name = $attr->name; - $value = $attr->value; - $errors = []; + /** + * Custom callback for additional attribute sanitization + * @internal + * + * @param \DOMAttr $attr + * @return array Array with exception objects for each modification + */ + public static function sanitizeAttr(DOMAttr $attr): array + { + $element = $attr->ownerElement; + $name = $attr->name; + $value = $attr->value; + $errors = []; - // block nested elements ("Billion Laughs" DoS attack) - if ( - $element->localName === 'use' && - Str::contains($name, 'href') !== false && - Str::startsWith($value, '#') === true - ) { - // find the target (used element) - $id = str_replace('"', '', mb_substr($value, 1)); - $target = (new DOMXPath($attr->ownerDocument))->query('//*[@id="' . $id . '"]')->item(0); + // block nested elements ("Billion Laughs" DoS attack) + if ( + $element->localName === 'use' && + Str::contains($name, 'href') !== false && + Str::startsWith($value, '#') === true + ) { + // find the target (used element) + $id = str_replace('"', '', mb_substr($value, 1)); + $target = (new DOMXPath($attr->ownerDocument))->query('//*[@id="' . $id . '"]')->item(0); - // the target must not contain any other elements - if ( - is_a($target, 'DOMElement') === true && - $target->getElementsByTagName('use')->count() > 0 - ) { - $errors[] = new InvalidArgumentException( - 'Nested "use" elements are not allowed' . - ' (used in line ' . $element->getLineNo() . ')' - ); - $element->removeAttributeNode($attr); - } - } + // the target must not contain any other elements + if ( + is_a($target, 'DOMElement') === true && + $target->getElementsByTagName('use')->count() > 0 + ) { + $errors[] = new InvalidArgumentException( + 'Nested "use" elements are not allowed' . + ' (used in line ' . $element->getLineNo() . ')' + ); + $element->removeAttributeNode($attr); + } + } - return $errors; - } + return $errors; + } - /** - * Custom callback for additional element sanitization - * @internal - * - * @param \DOMElement $element - * @return array Array with exception objects for each modification - */ - public static function sanitizeElement(DOMElement $element): array - { - $errors = []; + /** + * Custom callback for additional element sanitization + * @internal + * + * @param \DOMElement $element + * @return array Array with exception objects for each modification + */ + public static function sanitizeElement(DOMElement $element): array + { + $errors = []; - // check for URLs inside - * - * text - * - * @param string $string - * @return string - */ - public static function css($string) - { - return static::escaper()->escapeCss($string); - } + /** + * Escape HTML style property values + * + * This can be used to put untrusted data into a stylesheet or a style tag. + * + * Stay away from putting untrusted data into complex properties like url, + * behavior, and custom (-moz-binding). You should also not put untrusted data + * into IE’s expression property value which allows JavaScript. + * + * + * + * text + * + * @param string $string + * @return string + */ + public static function css($string) + { + return static::escaper()->escapeCss($string); + } - /** - * Get the escaper instance (and create if needed) - * - * @return \Laminas\Escaper\Escaper - */ - protected static function escaper() - { - return static::$escaper ??= new Escaper('utf-8'); - } + /** + * Get the escaper instance (and create if needed) + * + * @return \Laminas\Escaper\Escaper + */ + protected static function escaper() + { + return static::$escaper ??= new Escaper('utf-8'); + } - /** - * Escape HTML element content - * - * This can be used to put untrusted data directly into the HTML body somewhere. - * This includes inside normal tags like div, p, b, td, etc. - * - * Escapes &, <, >, ", and ' with HTML entity encoding to prevent switching - * into any execution context, such as script, style, or event handlers. - * - * ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE... - *
    ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...
    - * - * @param string $string - * @return string - */ - public static function html($string) - { - return static::escaper()->escapeHtml($string); - } + /** + * Escape HTML element content + * + * This can be used to put untrusted data directly into the HTML body somewhere. + * This includes inside normal tags like div, p, b, td, etc. + * + * Escapes &, <, >, ", and ' with HTML entity encoding to prevent switching + * into any execution context, such as script, style, or event handlers. + * + * ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE... + *
    ...ESCAPE UNTRUSTED DATA BEFORE PUTTING HERE...
    + * + * @param string $string + * @return string + */ + public static function html($string) + { + return static::escaper()->escapeHtml($string); + } - /** - * Escape JavaScript data values - * - * This can be used to put dynamically generated JavaScript code - * into both script blocks and event-handler attributes. - * - * - * - *
    - * - * @param string $string - * @return string - */ - public static function js($string) - { - return static::escaper()->escapeJs($string); - } + /** + * Escape JavaScript data values + * + * This can be used to put dynamically generated JavaScript code + * into both script blocks and event-handler attributes. + * + * + * + *
    + * + * @param string $string + * @return string + */ + public static function js($string) + { + return static::escaper()->escapeJs($string); + } - /** - * Escape URL parameter values - * - * This can be used to put untrusted data into HTTP GET parameter values. - * This should not be used to escape an entire URI. - * - * link - * - * @param string $string - * @return string - */ - public static function url($string) - { - return rawurlencode($string); - } + /** + * Escape URL parameter values + * + * This can be used to put untrusted data into HTTP GET parameter values. + * This should not be used to escape an entire URI. + * + * link + * + * @param string $string + * @return string + */ + public static function url($string) + { + return rawurlencode($string); + } - /** - * Escape XML element content - * - * Removes offending characters that could be wrongfully interpreted as XML markup. - * - * The following characters are reserved in XML and will be replaced with their - * corresponding XML entities: - * - * ' is replaced with ' - * " is replaced with " - * & is replaced with & - * < is replaced with < - * > is replaced with > - * - * @param string $string - * @return string - */ - public static function xml($string) - { - return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8'); - } + /** + * Escape XML element content + * + * Removes offending characters that could be wrongfully interpreted as XML markup. + * + * The following characters are reserved in XML and will be replaced with their + * corresponding XML entities: + * + * ' is replaced with ' + * " is replaced with " + * & is replaced with & + * < is replaced with < + * > is replaced with > + * + * @param string $string + * @return string + */ + public static function xml($string) + { + return htmlspecialchars($string, ENT_QUOTES | ENT_XML1, 'UTF-8'); + } } diff --git a/kirby/src/Toolkit/Facade.php b/kirby/src/Toolkit/Facade.php index 6b3b10c..bf72974 100644 --- a/kirby/src/Toolkit/Facade.php +++ b/kirby/src/Toolkit/Facade.php @@ -14,23 +14,23 @@ namespace Kirby\Toolkit; */ abstract class Facade { - /** - * Returns the instance that should be - * available statically - * - * @return mixed - */ - abstract public static function instance(); + /** + * Returns the instance that should be + * available statically + * + * @return mixed + */ + abstract public static function instance(); - /** - * Proxy for all public instance calls - * - * @param string $method - * @param array $args - * @return mixed - */ - public static function __callStatic(string $method, array $args = null) - { - return static::instance()->$method(...$args); - } + /** + * Proxy for all public instance calls + * + * @param string $method + * @param array $args + * @return mixed + */ + public static function __callStatic(string $method, array $args = null) + { + return static::instance()->$method(...$args); + } } diff --git a/kirby/src/Toolkit/Html.php b/kirby/src/Toolkit/Html.php index 7a699e3..c8c30d7 100644 --- a/kirby/src/Toolkit/Html.php +++ b/kirby/src/Toolkit/Html.php @@ -17,617 +17,621 @@ use Kirby\Http\Url; */ class Html extends Xml { - /** - * An internal store for an HTML entities translation table - * - * @var array - */ - public static $entities; - - /** - * List of HTML tags that can be used inline - * - * @var array - */ - public static $inlineList = [ - 'b', - 'i', - 'small', - 'abbr', - 'cite', - 'code', - 'dfn', - 'em', - 'kbd', - 'strong', - 'samp', - 'var', - 'a', - 'bdo', - 'br', - 'img', - 'q', - 'span', - 'sub', - 'sup' - ]; - - /** - * Closing string for void tags; - * can be used to switch to trailing slashes if required - * - * ```php - * Html::$void = ' />' - * ``` - * - * @var string - */ - public static $void = '>'; - - /** - * List of HTML tags that are considered to be self-closing - * - * @var array - */ - public static $voidList = [ - 'area', - 'base', - 'br', - 'col', - 'command', - 'embed', - 'hr', - 'img', - 'input', - 'keygen', - 'link', - 'meta', - 'param', - 'source', - 'track', - 'wbr' - ]; - - /** - * Generic HTML tag generator - * Can be called like `Html::p('A paragraph', ['class' => 'text'])` - * - * @param string $tag Tag name - * @param array $arguments Further arguments for the Html::tag() method - * @return string - */ - public static function __callStatic(string $tag, array $arguments = []): string - { - if (static::isVoid($tag) === true) { - return static::tag($tag, null, ...$arguments); - } - - return static::tag($tag, ...$arguments); - } - - /** - * Generates an `` tag; automatically supports mailto: and tel: links - * - * @param string $href The URL for the `` tag - * @param string|array|null $text The optional text; if `null`, the URL will be used as text - * @param array $attr Additional attributes for the tag - * @return string The generated HTML - */ - public static function a(string $href, $text = null, array $attr = []): string - { - if (Str::startsWith($href, 'mailto:')) { - return static::email(substr($href, 7), $text, $attr); - } - - if (Str::startsWith($href, 'tel:')) { - return static::tel(substr($href, 4), $text, $attr); - } - - return static::link($href, $text, $attr); - } - - /** - * Generates a single attribute or a list of attributes - * - * @param string|array $name String: A single attribute with that name will be generated. - * Key-value array: A list of attributes will be generated. Don't pass a second argument in that case. - * @param mixed $value If used with a `$name` string, pass the value of the attribute here. - * If used with a `$name` array, this can be set to `false` to disable attribute sorting. - * @return string|null The generated HTML attributes string - */ - public static function attr($name, $value = null): ?string - { - // HTML supports boolean attributes without values - if (is_array($name) === false && is_bool($value) === true) { - return $value === true ? strtolower($name) : null; - } - - // all other cases can share the XML variant - $attr = parent::attr($name, $value); - - if ($attr === null) { - return null; - } - - // HTML supports named entities - $entities = parent::entities(); - $html = array_keys($entities); - $xml = array_values($entities); - return str_replace($xml, $html, $attr); - } - - /** - * Converts lines in a string into HTML breaks - * - * @param string $string - * @return string - */ - public static function breaks(string $string): string - { - return nl2br($string); - } - - /** - * Generates an `` tag with `mailto:` - * - * @param string $email The email address - * @param string|array|null $text The optional text; if `null`, the email address will be used as text - * @param array $attr Additional attributes for the tag - * @return string The generated HTML - */ - public static function email(string $email, $text = null, array $attr = []): string - { - if (empty($email) === true) { - return ''; - } - - if (empty($text) === true) { - // show only the email address without additional parameters - $address = Str::contains($email, '?') ? Str::before($email, '?') : $email; - - $text = [Str::encode($address)]; - } - - $email = Str::encode($email); - $attr = array_merge([ - 'href' => [ - 'value' => 'mailto:' . $email, - 'escape' => false - ] - ], $attr); - - // add rel=noopener to target blank links to improve security - $attr['rel'] = static::rel($attr['rel'] ?? null, $attr['target'] ?? null); - - return static::tag('a', $text, $attr); - } - - /** - * Converts a string to an HTML-safe string - * - * @param string|null $string - * @param bool $keepTags If true, existing tags won't be escaped - * @return string The HTML string - * - * @psalm-suppress ParamNameMismatch - */ - public static function encode(?string $string, bool $keepTags = false): string - { - if ($string === null) { - return ''; - } - - if ($keepTags === true) { - $list = static::entities(); - unset($list['"'], $list['<'], $list['>'], $list['&']); - - $search = array_keys($list); - $values = array_values($list); - - return str_replace($search, $values, $string); - } - - return htmlentities($string, ENT_QUOTES, 'utf-8'); - } - - /** - * Returns the entity translation table - * - * @return array - */ - public static function entities(): array - { - return self::$entities ??= get_html_translation_table(HTML_ENTITIES); - } - - /** - * Creates a `
    ` tag with optional caption - * - * @param string|array $content Contents of the `
    ` tag - * @param string|array $caption Optional `
    ` text to use - * @param array $attr Additional attributes for the `
    ` tag - * @return string The generated HTML - */ - public static function figure($content, $caption = '', array $attr = []): string - { - if ($caption) { - $figcaption = static::tag('figcaption', $caption); - - if (is_string($content) === true) { - $content = [static::encode($content, false)]; - } - - $content[] = $figcaption; - } - - return static::tag('figure', $content, $attr); - } - - /** - * Embeds a GitHub Gist - * - * @param string $url Gist URL - * @param string|null $file Optional specific file to embed - * @param array $attr Additional attributes for the ` - + $js): ?> + + + +